aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/build.yml27
-rw-r--r--CMakeLists.txt25
-rw-r--r--launcher/Application.cpp113
-rw-r--r--launcher/Application.h15
-rw-r--r--launcher/CMakeLists.txt57
-rw-r--r--launcher/InstanceImportTask.cpp4
-rw-r--r--launcher/LaunchController.cpp6
-rw-r--r--launcher/ModDownloadTask.cpp72
-rw-r--r--launcher/QObjectPtr.h20
-rw-r--r--launcher/ResourceDownloadTask.cpp90
-rw-r--r--launcher/ResourceDownloadTask.h (renamed from launcher/ModDownloadTask.h)24
-rw-r--r--launcher/UpdateController.cpp443
-rw-r--r--launcher/UpdateController.h44
-rw-r--r--launcher/java/JavaInstallList.cpp4
-rw-r--r--launcher/launch/steps/CheckJava.cpp2
-rw-r--r--launcher/meta/BaseEntity.cpp2
-rw-r--r--launcher/minecraft/AssetsUtils.cpp2
-rw-r--r--launcher/minecraft/ComponentUpdateTask.cpp2
-rw-r--r--launcher/minecraft/MinecraftInstance.cpp39
-rw-r--r--launcher/minecraft/MinecraftUpdate.cpp8
-rw-r--r--launcher/minecraft/OneSixVersionFormat.cpp11
-rw-r--r--launcher/minecraft/PackProfile.cpp48
-rw-r--r--launcher/minecraft/PackProfile.h8
-rw-r--r--launcher/minecraft/auth/MinecraftAccount.cpp4
-rw-r--r--launcher/minecraft/auth/flows/MSA.cpp36
-rw-r--r--launcher/minecraft/auth/flows/Mojang.cpp16
-rw-r--r--launcher/minecraft/auth/flows/Offline.cpp4
-rw-r--r--launcher/minecraft/launch/LauncherPartLaunch.cpp1
-rw-r--r--launcher/minecraft/mod/ResourceFolderModel.cpp8
-rw-r--r--launcher/minecraft/mod/ResourcePackFolderModel.cpp2
-rw-r--r--launcher/minecraft/mod/TexturePackFolderModel.cpp2
-rw-r--r--launcher/minecraft/mod/tasks/BasicFolderLoadTask.h8
-rw-r--r--launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp4
-rw-r--r--launcher/minecraft/mod/tasks/LocalResourceParse.cpp18
-rw-r--r--launcher/minecraft/mod/tasks/LocalResourceParse.h6
-rw-r--r--launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp8
-rw-r--r--launcher/minecraft/update/AssetUpdateTask.cpp2
-rw-r--r--launcher/minecraft/update/FMLLibrariesTask.cpp10
-rw-r--r--launcher/minecraft/update/LibrariesTask.cpp2
-rw-r--r--launcher/modplatform/CheckUpdateTask.h14
-rw-r--r--launcher/modplatform/EnsureMetadataTask.cpp102
-rw-r--r--launcher/modplatform/EnsureMetadataTask.h18
-rw-r--r--launcher/modplatform/ModAPI.h118
-rw-r--r--launcher/modplatform/ModIndex.cpp24
-rw-r--r--launcher/modplatform/ModIndex.h40
-rw-r--r--launcher/modplatform/ResourceAPI.h177
-rw-r--r--launcher/modplatform/atlauncher/ATLPackInstallTask.cpp19
-rw-r--r--launcher/modplatform/flame/FileResolvingTask.cpp4
-rw-r--r--launcher/modplatform/flame/FlameAPI.cpp53
-rw-r--r--launcher/modplatform/flame/FlameAPI.h110
-rw-r--r--launcher/modplatform/flame/FlameCheckUpdate.cpp14
-rw-r--r--launcher/modplatform/flame/FlameCheckUpdate.h2
-rw-r--r--launcher/modplatform/flame/FlameInstanceCreationTask.cpp8
-rw-r--r--launcher/modplatform/flame/FlameInstanceCreationTask.h2
-rw-r--r--launcher/modplatform/flame/FlameModIndex.cpp8
-rw-r--r--launcher/modplatform/flame/FlameModIndex.h2
-rw-r--r--launcher/modplatform/helpers/HashUtils.cpp24
-rw-r--r--launcher/modplatform/helpers/HashUtils.h10
-rw-r--r--launcher/modplatform/helpers/NetworkModAPI.cpp97
-rw-r--r--launcher/modplatform/helpers/NetworkModAPI.h17
-rw-r--r--launcher/modplatform/helpers/NetworkResourceAPI.cpp128
-rw-r--r--launcher/modplatform/helpers/NetworkResourceAPI.h22
-rw-r--r--launcher/modplatform/legacy_ftb/PackFetchTask.cpp2
-rw-r--r--launcher/modplatform/legacy_ftb/PackInstallTask.cpp2
-rw-r--r--launcher/modplatform/modpacksch/FTBPackInstallTask.cpp22
-rw-r--r--launcher/modplatform/modrinth/ModrinthAPI.cpp91
-rw-r--r--launcher/modplatform/modrinth/ModrinthAPI.h134
-rw-r--r--launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp27
-rw-r--r--launcher/modplatform/modrinth/ModrinthCheckUpdate.h4
-rw-r--r--launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp21
-rw-r--r--launcher/modplatform/modrinth/ModrinthPackIndex.cpp11
-rw-r--r--launcher/modplatform/modrinth/ModrinthPackIndex.h2
-rw-r--r--launcher/modplatform/packwiz/Packwiz.cpp8
-rw-r--r--launcher/modplatform/packwiz/Packwiz.h2
-rw-r--r--launcher/modplatform/technic/SingleZipPackInstallTask.cpp4
-rw-r--r--launcher/modplatform/technic/SolderPackInstallTask.cpp6
-rw-r--r--launcher/net/Download.cpp11
-rw-r--r--launcher/net/Download.h3
-rw-r--r--launcher/net/MetaCacheSink.cpp1
-rw-r--r--launcher/net/NetAction.h4
-rw-r--r--launcher/net/NetJob.cpp5
-rw-r--r--launcher/net/PasteUpload.cpp2
-rw-r--r--launcher/net/Upload.cpp2
-rw-r--r--launcher/net/Upload.h2
-rw-r--r--launcher/news/NewsChecker.cpp6
-rw-r--r--launcher/tasks/ConcurrentTask.cpp6
-rw-r--r--launcher/tasks/ConcurrentTask.h2
-rw-r--r--launcher/translations/TranslationsModel.cpp4
-rw-r--r--launcher/ui/GuiUtil.cpp1
-rw-r--r--launcher/ui/MainWindow.cpp1118
-rw-r--r--launcher/ui/MainWindow.h40
-rw-r--r--launcher/ui/MainWindow.ui690
-rw-r--r--launcher/ui/dialogs/BlockedModsDialog.cpp8
-rw-r--r--launcher/ui/dialogs/ChooseProviderDialog.cpp6
-rw-r--r--launcher/ui/dialogs/ChooseProviderDialog.h6
-rw-r--r--launcher/ui/dialogs/ExportInstanceDialog.cpp1
-rw-r--r--launcher/ui/dialogs/ImportResourceDialog.cpp (renamed from launcher/ui/dialogs/ImportResourcePackDialog.cpp)19
-rw-r--r--launcher/ui/dialogs/ImportResourceDialog.h30
-rw-r--r--launcher/ui/dialogs/ImportResourceDialog.ui (renamed from launcher/ui/dialogs/ImportResourcePackDialog.ui)17
-rw-r--r--launcher/ui/dialogs/ImportResourcePackDialog.h27
-rw-r--r--launcher/ui/dialogs/ModDownloadDialog.cpp202
-rw-r--r--launcher/ui/dialogs/ModDownloadDialog.h78
-rw-r--r--launcher/ui/dialogs/ModUpdateDialog.cpp66
-rw-r--r--launcher/ui/dialogs/ModUpdateDialog.h14
-rw-r--r--launcher/ui/dialogs/ResourceDownloadDialog.cpp232
-rw-r--r--launcher/ui/dialogs/ResourceDownloadDialog.h111
-rw-r--r--launcher/ui/dialogs/ReviewMessageBox.cpp28
-rw-r--r--launcher/ui/dialogs/ReviewMessageBox.h11
-rw-r--r--launcher/ui/dialogs/ReviewMessageBox.ui14
-rw-r--r--launcher/ui/dialogs/UpdateDialog.cpp216
-rw-r--r--launcher/ui/dialogs/UpdateDialog.h67
-rw-r--r--launcher/ui/dialogs/UpdateDialog.ui91
-rw-r--r--launcher/ui/pages/global/LauncherPage.cpp122
-rw-r--r--launcher/ui/pages/global/LauncherPage.h12
-rw-r--r--launcher/ui/pages/global/LauncherPage.ui28
-rw-r--r--launcher/ui/pages/instance/LogPage.cpp2
-rw-r--r--launcher/ui/pages/instance/ManagedPackPage.cpp2
-rw-r--r--launcher/ui/pages/instance/ManagedPackPage.h4
-rw-r--r--launcher/ui/pages/instance/ModFolderPage.cpp8
-rw-r--r--launcher/ui/pages/instance/ResourcePackPage.h1
-rw-r--r--launcher/ui/pages/instance/ScreenshotsPage.h1
-rw-r--r--launcher/ui/pages/instance/ServersPage.cpp1
-rw-r--r--launcher/ui/pages/instance/VersionPage.cpp2
-rw-r--r--launcher/ui/pages/instance/VersionPage.h2
-rw-r--r--launcher/ui/pages/instance/WorldListPage.cpp2
-rw-r--r--launcher/ui/pages/modplatform/ModModel.cpp348
-rw-r--r--launcher/ui/pages/modplatform/ModModel.h94
-rw-r--r--launcher/ui/pages/modplatform/ModPage.cpp361
-rw-r--r--launcher/ui/pages/modplatform/ModPage.h97
-rw-r--r--launcher/ui/pages/modplatform/ResourceModel.cpp444
-rw-r--r--launcher/ui/pages/modplatform/ResourceModel.h147
-rw-r--r--launcher/ui/pages/modplatform/ResourcePage.cpp411
-rw-r--r--launcher/ui/pages/modplatform/ResourcePage.h111
-rw-r--r--launcher/ui/pages/modplatform/ResourcePage.ui (renamed from launcher/ui/pages/modplatform/ModPage.ui)18
-rw-r--r--launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp6
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameModModel.cpp31
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameModModel.h26
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameModel.cpp6
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp37
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameResourceModels.h30
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp (renamed from launcher/ui/pages/modplatform/flame/FlameModPage.cpp)44
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameResourcePages.h (renamed from launcher/ui/pages/modplatform/flame/FlameModPage.h)43
-rw-r--r--launcher/ui/pages/modplatform/ftb/FtbListModel.cpp18
-rw-r--r--launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp2
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp7
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthModel.h1
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp (renamed from launcher/ui/pages/modplatform/modrinth/ModrinthModModel.cpp)26
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h (renamed from launcher/ui/pages/modplatform/modrinth/ModrinthModModel.h)26
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp (renamed from launcher/ui/pages/modplatform/modrinth/ModrinthModPage.cpp)60
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h (renamed from launcher/ui/pages/modplatform/modrinth/ModrinthModPage.h)41
-rw-r--r--launcher/ui/pages/modplatform/technic/TechnicModel.cpp6
-rw-r--r--launcher/ui/pages/modplatform/technic/TechnicPage.cpp8
-rw-r--r--launcher/ui/widgets/ProgressWidget.cpp11
-rw-r--r--launcher/ui/widgets/ProgressWidget.h6
-rw-r--r--launcher/ui/widgets/WideBar.cpp58
-rw-r--r--launcher/ui/widgets/WideBar.h10
-rw-r--r--launcher/updater/DownloadTask.cpp177
-rw-r--r--launcher/updater/DownloadTask.h100
-rw-r--r--launcher/updater/GoUpdate.cpp198
-rw-r--r--launcher/updater/GoUpdate.h125
-rw-r--r--launcher/updater/MacSparkleUpdater.h2
-rw-r--r--launcher/updater/MacSparkleUpdater.mm12
-rw-r--r--launcher/updater/UpdateChecker.cpp296
-rw-r--r--launcher/updater/UpdateChecker.h140
m---------libraries/cmark0
-rw-r--r--tests/CMakeLists.txt3
-rw-r--r--tests/DummyResourceAPI.h46
-rw-r--r--tests/Packwiz_test.cpp4
-rw-r--r--tests/ResourceModel_test.cpp88
-rw-r--r--tests/Task_test.cpp174
170 files changed, 4430 insertions, 5343 deletions
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 5d4004d0..86e88fa1 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -145,10 +145,19 @@ jobs:
- name: Setup ccache
if: (runner.os != 'Windows' || matrix.msystem == '') && inputs.build_type == 'Debug'
- uses: hendrikmuhs/ccache-action@v1.2.7
+ uses: hendrikmuhs/ccache-action@v1.2.8
with:
key: ${{ matrix.os }}-qt${{ matrix.qt_ver }}-${{ matrix.architecture }}
+ - name: Retrieve ccache cache (Windows MinGW-w64)
+ if: runner.os == 'Windows' && matrix.msystem != '' && inputs.build_type == 'Debug'
+ uses: actions/cache@v3.2.4
+ with:
+ path: '${{ github.workspace }}\.ccache'
+ key: ${{ matrix.os }}-mingw-w64-ccache-${{ github.run_id }}
+ restore-keys: |
+ ${{ matrix.os }}-mingw-w64-ccache
+
- name: Setup ccache (Windows MinGW-w64)
if: runner.os == 'Windows' && matrix.msystem != '' && inputs.build_type == 'Debug'
shell: msys2 {0}
@@ -165,15 +174,6 @@ jobs:
run: |
echo "CCACHE_VAR=ccache" >> $GITHUB_ENV
- - name: Retrieve ccache cache (Windows MinGW-w64)
- if: runner.os == 'Windows' && matrix.msystem != '' && inputs.build_type == 'Debug'
- uses: actions/cache@v3.2.3
- with:
- path: '${{ github.workspace }}\.ccache'
- key: ${{ matrix.os }}-mingw-w64
- restore-keys: |
- ${{ matrix.os }}-mingw-w64
-
- name: Set short version
shell: bash
run: |
@@ -510,6 +510,13 @@ jobs:
with:
name: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage
path: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage
+
+ - name: ccache stats (Windows MinGW-w64)
+ if: runner.os == 'Windows' && matrix.msystem != ''
+ shell: msys2 {0}
+ run: |
+ ccache -s
+
snap:
runs-on: ubuntu-20.04
steps:
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 2194317b..37bb49ba 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -208,9 +208,15 @@ set(Launcher_BUILD_TIMESTAMP "${TODAY}")
################################ 3rd Party Libs ################################
-if(NOT Launcher_FORCE_BUNDLED_LIBS)
+# Successive configurations of cmake without cleaning the build dir will cause zlib fallback to fail due to cached values
+# Record when fallback triggered and skip this find_package
+if(NOT Launcher_FORCE_BUNDLED_LIBS AND NOT FORCE_BUNDLED_ZLIB)
find_package(ZLIB QUIET)
endif()
+if(NOT ZLIB_FOUND)
+ set(FORCE_BUNDLED_ZLIB TRUE CACHE BOOL "")
+ mark_as_advanced(FORCE_BUNDLED_ZLIB)
+endif()
# Find the required Qt parts
include(QtVersionlessBackport)
@@ -379,13 +385,24 @@ add_subdirectory(libraries/libnbtplusplus)
add_subdirectory(libraries/systeminfo) # system information library
add_subdirectory(libraries/launcher) # java based launcher part for Minecraft
add_subdirectory(libraries/javacheck) # java compatibility checker
-if(NOT ZLIB_FOUND)
+if(FORCE_BUNDLED_ZLIB)
message(STATUS "Using bundled zlib")
+
set(CMAKE_POLICY_DEFAULT_CMP0069 NEW) # Suppress cmake warnings and allow INTERPROCEDURAL_OPTIMIZATION for zlib
set(SKIP_INSTALL_ALL ON)
add_subdirectory(libraries/zlib EXCLUDE_FROM_ALL)
-
- set(ZLIB_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/libraries/zlib" "${CMAKE_CURRENT_BINARY_DIR}/libraries/zlib" CACHE STRING "")
+
+ # On OS where unistd.h exists, zlib's generated header defines `Z_HAVE_UNISTD_H`, while the included header does not.
+ # We cannot safely undo the rename on those systems, and they generally have packages for zlib anyway.
+ check_include_file(unistd.h NEED_GENERATED_ZCONF)
+ if (EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/libraries/zlib/zconf.h.included" AND NOT NEED_GENERATED_ZCONF)
+ # zlib's cmake script renames a file, dirtying the submodule, see https://github.com/madler/zlib/issues/162
+ message(STATUS "Undoing Rename")
+ message(STATUS " ${CMAKE_CURRENT_SOURCE_DIR}/libraries/zlib/zconf.h")
+ file(RENAME "${CMAKE_CURRENT_SOURCE_DIR}/libraries/zlib/zconf.h.included" "${CMAKE_CURRENT_SOURCE_DIR}/libraries/zlib/zconf.h")
+ endif()
+
+ set(ZLIB_INCLUDE_DIR "${CMAKE_CURRENT_BINARY_DIR}/libraries/zlib" "${CMAKE_CURRENT_SOURCE_DIR}/libraries/zlib" CACHE STRING "" FORCE)
set_target_properties(zlibstatic PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${ZLIB_INCLUDE_DIR}")
add_library(ZLIB::ZLIB ALIAS zlibstatic)
set(ZLIB_LIBRARY ZLIB::ZLIB CACHE STRING "zlib library name")
diff --git a/launcher/Application.cpp b/launcher/Application.cpp
index ed8d8d2c..0d3b086f 100644
--- a/launcher/Application.cpp
+++ b/launcher/Application.cpp
@@ -77,6 +77,7 @@
#include "ApplicationMessage.h"
#include <iostream>
+#include <mutex>
#include <QAccessible>
#include <QCommandLineParser>
@@ -101,7 +102,7 @@
#include "java/JavaUtils.h"
-#include "updater/UpdateChecker.h"
+#include "updater/ExternalUpdater.h"
#include "tools/JProfiler.h"
#include "tools/JVisualVM.h"
@@ -125,6 +126,10 @@
#include "MangoHud.h"
#endif
+#ifdef Q_OS_MAC
+#include "updater/MacSparkleUpdater.h"
+#endif
+
#if defined Q_OS_WIN32
#ifndef WIN32_LEAN_AND_MEAN
@@ -146,6 +151,9 @@ namespace {
/** This is used so that we can output to the log file in addition to the CLI. */
void appDebugOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg)
{
+ static std::mutex loggerMutex;
+ const std::lock_guard<std::mutex> lock(loggerMutex); // synchronized, QFile logFile is not thread-safe
+
QString out = qFormatLogMessage(type, context, msg);
out += QChar::LineFeed;
@@ -155,45 +163,6 @@ void appDebugOutput(QtMsgType type, const QMessageLogContext &context, const QSt
fflush(stderr);
}
-QString getIdealPlatform(QString currentPlatform) {
- auto info = Sys::getKernelInfo();
- switch(info.kernelType) {
- case Sys::KernelType::Darwin: {
- if(info.kernelMajor >= 17) {
- // macOS 10.13 or newer
- return "osx64-5.15.2";
- }
- else {
- // macOS 10.12 or older
- return "osx64";
- }
- }
- case Sys::KernelType::Windows: {
- // FIXME: 5.15.2 is not stable on Windows, due to a large number of completely unpredictable and hard to reproduce issues
- break;
-/*
- if(info.kernelMajor == 6 && info.kernelMinor >= 1) {
- // Windows 7
- return "win32-5.15.2";
- }
- else if (info.kernelMajor > 6) {
- // Above Windows 7
- return "win32-5.15.2";
- }
- else {
- // Below Windows 7
- return "win32";
- }
-*/
- }
- case Sys::KernelType::Undetermined:
- case Sys::KernelType::Linux: {
- break;
- }
- }
- return currentPlatform;
-}
-
}
Application::Application(int &argc, char **argv) : QApplication(argc, argv)
@@ -255,9 +224,19 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
m_serverToJoin = parser.value("server");
m_profileToUse = parser.value("profile");
m_liveCheck = parser.isSet("alive");
- m_zipToImport = parser.value("import");
+
m_instanceIdToShowWindowOf = parser.value("show");
+ for (auto zip_path : parser.values("import")){
+ m_zipsToImport.append(QUrl::fromLocalFile(QFileInfo(zip_path).absoluteFilePath()));
+ }
+
+ // treat unspecified positional arguments as import urls
+ for (auto zip_path : parser.positionalArguments()) {
+ m_zipsToImport.append(QUrl::fromLocalFile(QFileInfo(zip_path).absoluteFilePath()));
+ }
+
+
// error if --launch is missing with --server or --profile
if((!m_serverToJoin.isEmpty() || !m_profileToUse.isEmpty()) && m_instanceIdToLaunch.isEmpty())
{
@@ -341,7 +320,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
}
/*
- * Establish the mechanism for communication with an already running PolyMC that uses the same data path.
+ * Establish the mechanism for communication with an already running PrismLauncher that uses the same data path.
* If there is one, tell it what the user actually wanted to do and exit.
* We want to initialize this before logging to avoid messing with the log of a potential already running copy.
*/
@@ -359,12 +338,14 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
activate.command = "activate";
m_peerInstance->sendMessage(activate.serialize(), timeout);
- if(!m_zipToImport.isEmpty())
+ if(!m_zipsToImport.isEmpty())
{
- ApplicationMessage import;
- import.command = "import";
- import.args.insert("path", m_zipToImport.toString());
- m_peerInstance->sendMessage(import.serialize(), timeout);
+ for (auto zip_url : m_zipsToImport) {
+ ApplicationMessage import;
+ import.command = "import";
+ import.args.insert("path", zip_url.toString());
+ m_peerInstance->sendMessage(import.serialize(), timeout);
+ }
}
}
else
@@ -491,10 +472,6 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
{
// Provide a fallback for migration from PolyMC
m_settings.reset(new INISettingsObject({ BuildConfig.LAUNCHER_CONFIGFILE, "polymc.cfg", "multimc.cfg" }, this));
- // Updates
- // Multiple channels are separated by spaces
- m_settings->registerSetting("UpdateChannel", BuildConfig.VERSION_CHANNEL);
- m_settings->registerSetting("AutoUpdate", true);
// Theming
m_settings->registerSetting("IconTheme", QString("pe_colored"));
@@ -706,7 +683,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
// initialize network access and proxy setup
{
- m_network = new QNetworkAccessManager();
+ m_network.reset(new QNetworkAccessManager());
QString proxyTypeStr = settings()->get("ProxyType").toString();
QString addr = settings()->get("ProxyAddr").toString();
int port = settings()->get("ProxyPort").value<qint16>();
@@ -728,10 +705,10 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
// initialize the updater
if(BuildConfig.UPDATER_ENABLED)
{
- auto platform = getIdealPlatform(BuildConfig.BUILD_PLATFORM);
- auto channelUrl = BuildConfig.UPDATER_BASE + platform + "/channels.json";
- qDebug() << "Initializing updater with platform: " << platform << " -- " << channelUrl;
- m_updateChecker.reset(new UpdateChecker(m_network, channelUrl, BuildConfig.VERSION_CHANNEL));
+ qDebug() << "Initializing updater";
+#ifdef Q_OS_MAC
+ m_updater.reset(new MacSparkleUpdater());
+#endif
qDebug() << "<> Updater started.";
}
@@ -938,7 +915,7 @@ bool Application::event(QEvent* event)
if (event->type() == QEvent::FileOpen) {
auto ev = static_cast<QFileOpenEvent*>(event);
- m_mainWindow->droppedURLs({ ev->url() });
+ m_mainWindow->processURLs({ ev->url() });
}
return QApplication::event(event);
@@ -998,10 +975,10 @@ void Application::performMainStartupAction()
showMainWindow(false);
qDebug() << "<> Main window shown.";
}
- if(!m_zipToImport.isEmpty())
+ if(!m_zipsToImport.isEmpty())
{
- qDebug() << "<> Importing instance from zip:" << m_zipToImport;
- m_mainWindow->droppedURLs({ m_zipToImport });
+ qDebug() << "<> Importing from zip:" << m_zipsToImport;
+ m_mainWindow->processURLs( m_zipsToImport );
}
}
@@ -1054,7 +1031,7 @@ void Application::messageReceived(const QByteArray& message)
qWarning() << "Received" << command << "message without a zip path/URL.";
return;
}
- m_mainWindow->droppedURLs({ QUrl(path) });
+ m_mainWindow->processURLs({ QUrl::fromLocalFile(QFileInfo(path).absoluteFilePath()) });
}
else if(command == "launch")
{
@@ -1535,7 +1512,8 @@ QString Application::getJarPath(QString jarFile)
FS::PathCombine(m_rootPath, "share/" + BuildConfig.LAUNCHER_APP_BINARY_NAME),
#endif
FS::PathCombine(m_rootPath, "jars"),
- FS::PathCombine(applicationDirPath(), "jars")
+ FS::PathCombine(applicationDirPath(), "jars"),
+ FS::PathCombine(applicationDirPath(), "..", "jars") // from inside build dir, for debuging
};
for(QString p : potentialPaths)
{
@@ -1685,3 +1663,14 @@ bool Application::handleDataMigration(const QString& currentData,
}
return true;
}
+
+void Application::triggerUpdateCheck()
+{
+ if (m_updater) {
+ qDebug() << "Checking for updates.";
+ m_updater->setBetaAllowed(false); // There are no other channels than stable
+ m_updater->checkForUpdates();
+ } else {
+ qDebug() << "Updater not available.";
+ }
+}
diff --git a/launcher/Application.h b/launcher/Application.h
index a7938629..1b3dc499 100644
--- a/launcher/Application.h
+++ b/launcher/Application.h
@@ -43,7 +43,6 @@
#include <QIcon>
#include <QDateTime>
#include <QUrl>
-#include <updater/GoUpdate.h>
#include <BaseInstance.h>
@@ -63,7 +62,7 @@ class AccountList;
class IconList;
class QNetworkAccessManager;
class JavaInstallList;
-class UpdateChecker;
+class ExternalUpdater;
class BaseProfilerFactory;
class BaseDetachedToolFactory;
class TranslationsModel;
@@ -126,10 +125,12 @@ public:
void setApplicationTheme(const QString& name);
- shared_qobject_ptr<UpdateChecker> updateChecker() {
- return m_updateChecker;
+ shared_qobject_ptr<ExternalUpdater> updater() {
+ return m_updater;
}
+ void triggerUpdateCheck();
+
std::shared_ptr<TranslationsModel> translations();
std::shared_ptr<JavaInstallList> javalist();
@@ -208,6 +209,7 @@ signals:
void updateAllowedChanged(bool status);
void globalSettingsAboutToOpen();
void globalSettingsClosed();
+ int currentCatChanged(int index);
#ifdef Q_OS_MACOS
void clickedOnDock();
@@ -250,7 +252,7 @@ private:
shared_qobject_ptr<QNetworkAccessManager> m_network;
- shared_qobject_ptr<UpdateChecker> m_updateChecker;
+ shared_qobject_ptr<ExternalUpdater> m_updater;
shared_qobject_ptr<AccountList> m_accounts;
shared_qobject_ptr<HttpMetaCache> m_metacache;
@@ -305,8 +307,7 @@ public:
QString m_serverToJoin;
QString m_profileToUse;
bool m_liveCheck = false;
- QUrl m_zipToImport;
+ QList<QUrl> m_zipsToImport;
QString m_instanceIdToShowWindowOf;
std::unique_ptr<QFile> logFile;
};
-
diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt
index eec6c787..1bfe9cbc 100644
--- a/launcher/CMakeLists.txt
+++ b/launcher/CMakeLists.txt
@@ -38,9 +38,9 @@ set(CORE_SOURCES
InstanceImportTask.h
InstanceImportTask.cpp
- # Mod downloading task
- ModDownloadTask.h
- ModDownloadTask.cpp
+ # Resource downloading task
+ ResourceDownloadTask.h
+ ResourceDownloadTask.cpp
# Use tracking separate from memory management
Usable.h
@@ -161,12 +161,6 @@ set(LAUNCH_SOURCES
# Old update system
set(UPDATE_SOURCES
- updater/GoUpdate.h
- updater/GoUpdate.cpp
- updater/UpdateChecker.h
- updater/UpdateChecker.cpp
- updater/DownloadTask.h
- updater/DownloadTask.cpp
updater/ExternalUpdater.h
)
@@ -473,7 +467,7 @@ set(API_SOURCES
modplatform/ModIndex.h
modplatform/ModIndex.cpp
- modplatform/ModAPI.h
+ modplatform/ResourceAPI.h
modplatform/EnsureMetadataTask.h
modplatform/EnsureMetadataTask.cpp
@@ -484,8 +478,8 @@ set(API_SOURCES
modplatform/flame/FlameAPI.cpp
modplatform/modrinth/ModrinthAPI.h
modplatform/modrinth/ModrinthAPI.cpp
- modplatform/helpers/NetworkModAPI.h
- modplatform/helpers/NetworkModAPI.cpp
+ modplatform/helpers/NetworkResourceAPI.h
+ modplatform/helpers/NetworkResourceAPI.cpp
modplatform/helpers/HashUtils.h
modplatform/helpers/HashUtils.cpp
modplatform/helpers/OverrideUtils.h
@@ -621,8 +615,6 @@ SET(LAUNCHER_SOURCES
Application.cpp
DataMigrationTask.h
DataMigrationTask.cpp
- UpdateController.cpp
- UpdateController.h
ApplicationMessage.h
ApplicationMessage.cpp
@@ -771,6 +763,11 @@ SET(LAUNCHER_SOURCES
ui/pages/modplatform/VanillaPage.cpp
ui/pages/modplatform/VanillaPage.h
+ ui/pages/modplatform/ResourcePage.cpp
+ ui/pages/modplatform/ResourcePage.h
+ ui/pages/modplatform/ResourceModel.cpp
+ ui/pages/modplatform/ResourceModel.h
+
ui/pages/modplatform/ModPage.cpp
ui/pages/modplatform/ModPage.h
ui/pages/modplatform/ModModel.cpp
@@ -803,10 +800,10 @@ SET(LAUNCHER_SOURCES
ui/pages/modplatform/flame/FlameModel.h
ui/pages/modplatform/flame/FlamePage.cpp
ui/pages/modplatform/flame/FlamePage.h
- ui/pages/modplatform/flame/FlameModModel.cpp
- ui/pages/modplatform/flame/FlameModModel.h
- ui/pages/modplatform/flame/FlameModPage.cpp
- ui/pages/modplatform/flame/FlameModPage.h
+ ui/pages/modplatform/flame/FlameResourceModels.cpp
+ ui/pages/modplatform/flame/FlameResourceModels.h
+ ui/pages/modplatform/flame/FlameResourcePages.cpp
+ ui/pages/modplatform/flame/FlameResourcePages.h
ui/pages/modplatform/modrinth/ModrinthPage.cpp
ui/pages/modplatform/modrinth/ModrinthPage.h
@@ -821,10 +818,10 @@ SET(LAUNCHER_SOURCES
ui/pages/modplatform/ImportPage.cpp
ui/pages/modplatform/ImportPage.h
- ui/pages/modplatform/modrinth/ModrinthModModel.cpp
- ui/pages/modplatform/modrinth/ModrinthModModel.h
- ui/pages/modplatform/modrinth/ModrinthModPage.cpp
- ui/pages/modplatform/modrinth/ModrinthModPage.h
+ ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp
+ ui/pages/modplatform/modrinth/ModrinthResourceModels.h
+ ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp
+ ui/pages/modplatform/modrinth/ModrinthResourcePages.h
# GUI - dialogs
ui/dialogs/AboutDialog.cpp
@@ -843,8 +840,8 @@ SET(LAUNCHER_SOURCES
ui/dialogs/ExportInstanceDialog.h
ui/dialogs/IconPickerDialog.cpp
ui/dialogs/IconPickerDialog.h
- ui/dialogs/ImportResourcePackDialog.cpp
- ui/dialogs/ImportResourcePackDialog.h
+ ui/dialogs/ImportResourceDialog.cpp
+ ui/dialogs/ImportResourceDialog.h
ui/dialogs/LoginDialog.cpp
ui/dialogs/LoginDialog.h
ui/dialogs/MSALoginDialog.cpp
@@ -863,14 +860,12 @@ SET(LAUNCHER_SOURCES
ui/dialogs/ProgressDialog.h
ui/dialogs/ReviewMessageBox.cpp
ui/dialogs/ReviewMessageBox.h
- ui/dialogs/UpdateDialog.cpp
- ui/dialogs/UpdateDialog.h
ui/dialogs/VersionSelectDialog.cpp
ui/dialogs/VersionSelectDialog.h
ui/dialogs/SkinUploadDialog.cpp
ui/dialogs/SkinUploadDialog.h
- ui/dialogs/ModDownloadDialog.cpp
- ui/dialogs/ModDownloadDialog.h
+ ui/dialogs/ResourceDownloadDialog.cpp
+ ui/dialogs/ResourceDownloadDialog.h
ui/dialogs/ScrollMessageBox.cpp
ui/dialogs/ScrollMessageBox.h
ui/dialogs/BlockedModsDialog.cpp
@@ -942,6 +937,7 @@ SET(LAUNCHER_SOURCES
)
qt_wrap_ui(LAUNCHER_UI
+ ui/MainWindow.ui
ui/setupwizard/PasteWizardPage.ui
ui/setupwizard/ThemeWizardPage.ui
ui/pages/global/AccountListPage.ui
@@ -965,7 +961,7 @@ qt_wrap_ui(LAUNCHER_UI
ui/pages/modplatform/atlauncher/AtlOptionalModDialog.ui
ui/pages/modplatform/atlauncher/AtlPage.ui
ui/pages/modplatform/VanillaPage.ui
- ui/pages/modplatform/ModPage.ui
+ ui/pages/modplatform/ResourcePage.ui
ui/pages/modplatform/flame/FlamePage.ui
ui/pages/modplatform/legacy_ftb/Page.ui
ui/pages/modplatform/ImportPage.ui
@@ -981,14 +977,13 @@ qt_wrap_ui(LAUNCHER_UI
ui/dialogs/ProfileSetupDialog.ui
ui/dialogs/ProgressDialog.ui
ui/dialogs/NewInstanceDialog.ui
- ui/dialogs/UpdateDialog.ui
ui/dialogs/NewComponentDialog.ui
ui/dialogs/NewsDialog.ui
ui/dialogs/ProfileSelectDialog.ui
ui/dialogs/SkinUploadDialog.ui
ui/dialogs/ExportInstanceDialog.ui
ui/dialogs/IconPickerDialog.ui
- ui/dialogs/ImportResourcePackDialog.ui
+ ui/dialogs/ImportResourceDialog.ui
ui/dialogs/MSALoginDialog.ui
ui/dialogs/OfflineLoginDialog.ui
ui/dialogs/AboutDialog.ui
diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp
index 6b3fd296..70bf5784 100644
--- a/launcher/InstanceImportTask.cpp
+++ b/launcher/InstanceImportTask.cpp
@@ -88,7 +88,7 @@ void InstanceImportTask::executeTask()
entry->setStale(true);
m_archivePath = entry->getFullPath();
- m_filesNetJob = new NetJob(tr("Modpack download"), APPLICATION->network());
+ m_filesNetJob.reset(new NetJob(tr("Modpack download"), APPLICATION->network()));
m_filesNetJob->addNetAction(Net::Download::makeCached(m_sourceUrl, entry));
connect(m_filesNetJob.get(), &NetJob::succeeded, this, &InstanceImportTask::downloadSucceeded);
@@ -301,7 +301,7 @@ void InstanceImportTask::processFlame()
void InstanceImportTask::processTechnic()
{
- shared_qobject_ptr<Technic::TechnicPackProcessor> packProcessor = new Technic::TechnicPackProcessor();
+ shared_qobject_ptr<Technic::TechnicPackProcessor> packProcessor{ new Technic::TechnicPackProcessor };
connect(packProcessor.get(), &Technic::TechnicPackProcessor::succeeded, this, &InstanceImportTask::emitSucceeded);
connect(packProcessor.get(), &Technic::TechnicPackProcessor::failed, this, &InstanceImportTask::emitFailed);
packProcessor->run(m_globalSettings, name(), m_instIcon, m_stagingPath);
diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp
index 9741fd95..070ee283 100644
--- a/launcher/LaunchController.cpp
+++ b/launcher/LaunchController.cpp
@@ -382,15 +382,15 @@ void LaunchController::launchInstance()
}
resolved_servers = resolved_servers + "]\n\n";
}
- m_launcher->prependStep(new TextPrint(m_launcher.get(), resolved_servers, MessageLevel::Launcher));
+ m_launcher->prependStep(makeShared<TextPrint>(m_launcher.get(), resolved_servers, MessageLevel::Launcher));
} else {
online_mode = m_demo ? "demo" : "offline";
}
- m_launcher->prependStep(new TextPrint(m_launcher.get(), "Launched instance in " + online_mode + " mode\n", MessageLevel::Launcher));
+ m_launcher->prependStep(makeShared<TextPrint>(m_launcher.get(), "Launched instance in " + online_mode + " mode\n", MessageLevel::Launcher));
// Prepend Version
- m_launcher->prependStep(new TextPrint(m_launcher.get(), BuildConfig.LAUNCHER_DISPLAYNAME + " version: " + BuildConfig.printableVersionString() + "\n\n", MessageLevel::Launcher));
+ m_launcher->prependStep(makeShared<TextPrint>(m_launcher.get(), BuildConfig.LAUNCHER_DISPLAYNAME + " version: " + BuildConfig.printableVersionString() + "\n\n", MessageLevel::Launcher));
m_launcher->start();
}
diff --git a/launcher/ModDownloadTask.cpp b/launcher/ModDownloadTask.cpp
deleted file mode 100644
index 2b0343f4..00000000
--- a/launcher/ModDownloadTask.cpp
+++ /dev/null
@@ -1,72 +0,0 @@
-// SPDX-License-Identifier: GPL-3.0-only
-/*
-* PolyMC - Minecraft Launcher
-* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
-* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
-*
-* This program is free software: you can redistribute it and/or modify
-* it under the terms of the GNU General Public License as published by
-* the Free Software Foundation, version 3.
-*
-* This program is distributed in the hope that it will be useful,
-* but WITHOUT ANY WARRANTY; without even the implied warranty of
-* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-* GNU General Public License for more details.
-*
-* You should have received a copy of the GNU General Public License
-* along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
-#include "ModDownloadTask.h"
-
-#include "Application.h"
-#include "minecraft/mod/ModFolderModel.h"
-
-ModDownloadTask::ModDownloadTask(ModPlatform::IndexedPack mod, ModPlatform::IndexedVersion version, const std::shared_ptr<ModFolderModel> mods, bool is_indexed)
- : m_mod(mod), m_mod_version(version), mods(mods)
-{
- if (is_indexed) {
- m_update_task.reset(new LocalModUpdateTask(mods->indexDir(), m_mod, m_mod_version));
- connect(m_update_task.get(), &LocalModUpdateTask::hasOldMod, this, &ModDownloadTask::hasOldMod);
-
- addTask(m_update_task);
- }
-
- m_filesNetJob.reset(new NetJob(tr("Mod download"), APPLICATION->network()));
- m_filesNetJob->setStatus(tr("Downloading mod:\n%1").arg(m_mod_version.downloadUrl));
-
- m_filesNetJob->addNetAction(Net::Download::makeFile(m_mod_version.downloadUrl, mods->dir().absoluteFilePath(getFilename())));
- connect(m_filesNetJob.get(), &NetJob::succeeded, this, &ModDownloadTask::downloadSucceeded);
- connect(m_filesNetJob.get(), &NetJob::progress, this, &ModDownloadTask::downloadProgressChanged);
- connect(m_filesNetJob.get(), &NetJob::failed, this, &ModDownloadTask::downloadFailed);
-
- addTask(m_filesNetJob);
-}
-
-void ModDownloadTask::downloadSucceeded()
-{
- m_filesNetJob.reset();
- auto name = std::get<0>(to_delete);
- auto filename = std::get<1>(to_delete);
- if (!name.isEmpty() && filename != m_mod_version.fileName) {
- mods->uninstallMod(filename, true);
- }
-}
-
-void ModDownloadTask::downloadFailed(QString reason)
-{
- emitFailed(reason);
- m_filesNetJob.reset();
-}
-
-void ModDownloadTask::downloadProgressChanged(qint64 current, qint64 total)
-{
- emit progress(current, total);
-}
-
-// This indirection is done so that we don't delete a mod before being sure it was
-// downloaded successfully!
-void ModDownloadTask::hasOldMod(QString name, QString filename)
-{
- to_delete = {name, filename};
-}
diff --git a/launcher/QObjectPtr.h b/launcher/QObjectPtr.h
index b1ef1c8d..a1c64b43 100644
--- a/launcher/QObjectPtr.h
+++ b/launcher/QObjectPtr.h
@@ -20,18 +20,34 @@ using unique_qobject_ptr = QScopedPointer<T, QScopedPointerDeleteLater>;
template <typename T>
class shared_qobject_ptr : public QSharedPointer<T> {
public:
- constexpr shared_qobject_ptr() : QSharedPointer<T>() {}
- constexpr shared_qobject_ptr(T* ptr) : QSharedPointer<T>(ptr, &QObject::deleteLater) {}
+ constexpr explicit shared_qobject_ptr() : QSharedPointer<T>() {}
+ constexpr explicit shared_qobject_ptr(T* ptr) : QSharedPointer<T>(ptr, &QObject::deleteLater) {}
constexpr shared_qobject_ptr(std::nullptr_t null_ptr) : QSharedPointer<T>(null_ptr, &QObject::deleteLater) {}
template <typename Derived>
constexpr shared_qobject_ptr(const shared_qobject_ptr<Derived>& other) : QSharedPointer<T>(other)
{}
+ template <typename Derived>
+ constexpr shared_qobject_ptr(const QSharedPointer<Derived>& other) : QSharedPointer<T>(other)
+ {}
+
void reset() { QSharedPointer<T>::reset(); }
+ void reset(T*&& other)
+ {
+ shared_qobject_ptr<T> t(other);
+ this->swap(t);
+ }
void reset(const shared_qobject_ptr<T>& other)
{
shared_qobject_ptr<T> t(other);
this->swap(t);
}
};
+
+template <typename T, typename... Args>
+shared_qobject_ptr<T> makeShared(Args... args)
+{
+ auto obj = new T(args...);
+ return shared_qobject_ptr<T>(obj);
+}
diff --git a/launcher/ResourceDownloadTask.cpp b/launcher/ResourceDownloadTask.cpp
new file mode 100644
index 00000000..98bcf259
--- /dev/null
+++ b/launcher/ResourceDownloadTask.cpp
@@ -0,0 +1,90 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+* Prism Launcher - Minecraft Launcher
+* Copyright (c) 2022-2023 flowln <flowlnlnln@gmail.com>
+* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+*
+* This program is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published by
+* the Free Software Foundation, version 3.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+#include "ResourceDownloadTask.h"
+
+#include "Application.h"
+
+#include "minecraft/mod/ModFolderModel.h"
+#include "minecraft/mod/ResourceFolderModel.h"
+
+ResourceDownloadTask::ResourceDownloadTask(ModPlatform::IndexedPack pack,
+ ModPlatform::IndexedVersion version,
+ const std::shared_ptr<ResourceFolderModel> packs,
+ bool is_indexed)
+ : m_pack(std::move(pack)), m_pack_version(std::move(version)), m_pack_model(packs)
+{
+ if (auto model = dynamic_cast<ModFolderModel*>(m_pack_model.get()); model && is_indexed) {
+ m_update_task.reset(new LocalModUpdateTask(model->indexDir(), m_pack, m_pack_version));
+ connect(m_update_task.get(), &LocalModUpdateTask::hasOldMod, this, &ResourceDownloadTask::hasOldResource);
+
+ addTask(m_update_task);
+ }
+
+ m_filesNetJob.reset(new NetJob(tr("Resource download"), APPLICATION->network()));
+ m_filesNetJob->setStatus(tr("Downloading resource:\n%1").arg(m_pack_version.downloadUrl));
+
+ QDir dir { m_pack_model->dir() };
+ {
+ // FIXME: Make this more generic. May require adding additional info to IndexedVersion,
+ // or adquiring a reference to the base instance.
+ if (!m_pack_version.custom_target_folder.isEmpty()) {
+ dir.cdUp();
+ dir.cd(m_pack_version.custom_target_folder);
+ }
+ }
+
+ m_filesNetJob->addNetAction(Net::Download::makeFile(m_pack_version.downloadUrl, dir.absoluteFilePath(getFilename())));
+ connect(m_filesNetJob.get(), &NetJob::succeeded, this, &ResourceDownloadTask::downloadSucceeded);
+ connect(m_filesNetJob.get(), &NetJob::progress, this, &ResourceDownloadTask::downloadProgressChanged);
+ connect(m_filesNetJob.get(), &NetJob::failed, this, &ResourceDownloadTask::downloadFailed);
+
+ addTask(m_filesNetJob);
+}
+
+void ResourceDownloadTask::downloadSucceeded()
+{
+ m_filesNetJob.reset();
+ auto name = std::get<0>(to_delete);
+ auto filename = std::get<1>(to_delete);
+ if (!name.isEmpty() && filename != m_pack_version.fileName) {
+ if (auto model = dynamic_cast<ModFolderModel*>(m_pack_model.get()); model)
+ model->uninstallMod(filename, true);
+ else
+ m_pack_model->uninstallResource(filename);
+ }
+}
+
+void ResourceDownloadTask::downloadFailed(QString reason)
+{
+ emitFailed(reason);
+ m_filesNetJob.reset();
+}
+
+void ResourceDownloadTask::downloadProgressChanged(qint64 current, qint64 total)
+{
+ emit progress(current, total);
+}
+
+// This indirection is done so that we don't delete a mod before being sure it was
+// downloaded successfully!
+void ResourceDownloadTask::hasOldResource(QString name, QString filename)
+{
+ to_delete = { name, filename };
+}
diff --git a/launcher/ModDownloadTask.h b/launcher/ResourceDownloadTask.h
index 95020470..73ad2d07 100644
--- a/launcher/ModDownloadTask.h
+++ b/launcher/ResourceDownloadTask.h
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
-* PolyMC - Minecraft Launcher
-* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
+* Prism Launcher - Minecraft Launcher
+* Copyright (c) 2022-2023 flowln <flowlnlnln@gmail.com>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -25,32 +25,32 @@
#include "modplatform/ModIndex.h"
#include "minecraft/mod/tasks/LocalModUpdateTask.h"
-class ModFolderModel;
+class ResourceFolderModel;
-class ModDownloadTask : public SequentialTask {
+class ResourceDownloadTask : public SequentialTask {
Q_OBJECT
public:
- explicit ModDownloadTask(ModPlatform::IndexedPack mod, ModPlatform::IndexedVersion version, const std::shared_ptr<ModFolderModel> mods, bool is_indexed = true);
- const QString& getFilename() const { return m_mod_version.fileName; }
+ explicit ResourceDownloadTask(ModPlatform::IndexedPack pack, ModPlatform::IndexedVersion version, const std::shared_ptr<ResourceFolderModel> packs, bool is_indexed = true);
+ const QString& getFilename() const { return m_pack_version.fileName; }
+ const QString& getCustomPath() const { return m_pack_version.custom_target_folder; }
+ const QVariant& getVersionID() const { return m_pack_version.fileId; }
private:
- ModPlatform::IndexedPack m_mod;
- ModPlatform::IndexedVersion m_mod_version;
- const std::shared_ptr<ModFolderModel> mods;
+ ModPlatform::IndexedPack m_pack;
+ ModPlatform::IndexedVersion m_pack_version;
+ const std::shared_ptr<ResourceFolderModel> m_pack_model;
NetJob::Ptr m_filesNetJob;
LocalModUpdateTask::Ptr m_update_task;
void downloadProgressChanged(qint64 current, qint64 total);
-
void downloadFailed(QString reason);
-
void downloadSucceeded();
std::tuple<QString, QString> to_delete {"", ""};
private slots:
- void hasOldMod(QString name, QString filename);
+ void hasOldResource(QString name, QString filename);
};
diff --git a/launcher/UpdateController.cpp b/launcher/UpdateController.cpp
deleted file mode 100644
index 9ff44854..00000000
--- a/launcher/UpdateController.cpp
+++ /dev/null
@@ -1,443 +0,0 @@
-#include <QFile>
-#include <QMessageBox>
-#include <FileSystem.h>
-#include <updater/GoUpdate.h>
-#include "UpdateController.h"
-#include <QApplication>
-#include <thread>
-#include <chrono>
-#include <LocalPeer.h>
-
-#include "BuildConfig.h"
-
-
-// from <sys/stat.h>
-#ifndef S_IRUSR
-#define __S_IREAD 0400 /* Read by owner. */
-#define __S_IWRITE 0200 /* Write by owner. */
-#define __S_IEXEC 0100 /* Execute by owner. */
-#define S_IRUSR __S_IREAD /* Read by owner. */
-#define S_IWUSR __S_IWRITE /* Write by owner. */
-#define S_IXUSR __S_IEXEC /* Execute by owner. */
-
-#define S_IRGRP (S_IRUSR >> 3) /* Read by group. */
-#define S_IWGRP (S_IWUSR >> 3) /* Write by group. */
-#define S_IXGRP (S_IXUSR >> 3) /* Execute by group. */
-
-#define S_IROTH (S_IRGRP >> 3) /* Read by others. */
-#define S_IWOTH (S_IWGRP >> 3) /* Write by others. */
-#define S_IXOTH (S_IXGRP >> 3) /* Execute by others. */
-#endif
-static QFile::Permissions unixModeToPermissions(const int mode)
-{
- QFile::Permissions perms;
-
- if (mode & S_IRUSR)
- {
- perms |= QFile::ReadUser;
- }
- if (mode & S_IWUSR)
- {
- perms |= QFile::WriteUser;
- }
- if (mode & S_IXUSR)
- {
- perms |= QFile::ExeUser;
- }
-
- if (mode & S_IRGRP)
- {
- perms |= QFile::ReadGroup;
- }
- if (mode & S_IWGRP)
- {
- perms |= QFile::WriteGroup;
- }
- if (mode & S_IXGRP)
- {
- perms |= QFile::ExeGroup;
- }
-
- if (mode & S_IROTH)
- {
- perms |= QFile::ReadOther;
- }
- if (mode & S_IWOTH)
- {
- perms |= QFile::WriteOther;
- }
- if (mode & S_IXOTH)
- {
- perms |= QFile::ExeOther;
- }
- return perms;
-}
-
-static const QLatin1String liveCheckFile("live.check");
-
-UpdateController::UpdateController(QWidget * parent, const QString& root, const QString updateFilesDir, GoUpdate::OperationList operations)
-{
- m_parent = parent;
- m_root = root;
- m_updateFilesDir = updateFilesDir;
- m_operations = operations;
-}
-
-
-void UpdateController::installUpdates()
-{
- qint64 pid = -1;
- QStringList args;
- bool started = false;
-
- qDebug() << "Installing updates.";
-#ifdef Q_OS_WIN
- QString finishCmd = QApplication::applicationFilePath();
-#elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined (Q_OS_OPENBSD)
- QString finishCmd = FS::PathCombine(m_root, BuildConfig.LAUNCHER_NAME);
-#elif defined Q_OS_MAC
- QString finishCmd = QApplication::applicationFilePath();
-#else
-#error Unsupported operating system.
-#endif
-
- QString backupPath = FS::PathCombine(m_root, "update", "backup");
- QDir origin(m_root);
-
- // clean up the backup folder. it should be empty before we start
- if(!FS::deletePath(backupPath))
- {
- qWarning() << "couldn't remove previous backup folder" << backupPath;
- }
- // and it should exist.
- if(!FS::ensureFolderPathExists(backupPath))
- {
- qWarning() << "couldn't create folder" << backupPath;
- return;
- }
-
- bool useXPHack = false;
- QString exePath;
- QString exeOrigin;
- QString exeBackup;
-
- // perform the update operations
- for(auto op: m_operations)
- {
- switch(op.type)
- {
- // replace = move original out to backup, if it exists, move the new file in its place
- case GoUpdate::Operation::OP_REPLACE:
- {
-#ifdef Q_OS_WIN32
- QString windowsExeName = BuildConfig.LAUNCHER_NAME + ".exe";
- // hack for people renaming the .exe because ... reasons :)
- if(op.destination == windowsExeName)
- {
- op.destination = QFileInfo(QApplication::applicationFilePath()).fileName();
- }
-#endif
- QFileInfo destination (FS::PathCombine(m_root, op.destination));
- if(destination.exists())
- {
- QString backupName = op.destination;
- backupName.replace('/', '_');
- QString backupFilePath = FS::PathCombine(backupPath, backupName);
- if(!QFile::rename(destination.absoluteFilePath(), backupFilePath))
- {
- qWarning() << "Couldn't move:" << destination.absoluteFilePath() << "to" << backupFilePath;
- m_failedOperationType = Replace;
- m_failedFile = op.destination;
- fail();
- return;
- }
- BackupEntry be;
- be.original = destination.absoluteFilePath();
- be.backup = backupFilePath;
- be.update = op.source;
- m_replace_backups.append(be);
- }
- // make sure the folder we are putting this into exists
- if(!FS::ensureFilePathExists(destination.absoluteFilePath()))
- {
- qWarning() << "REPLACE: Couldn't create folder:" << destination.absoluteFilePath();
- m_failedOperationType = Replace;
- m_failedFile = op.destination;
- fail();
- return;
- }
- // now move the new file in
- if(!QFile::rename(op.source, destination.absoluteFilePath()))
- {
- qWarning() << "REPLACE: Couldn't move:" << op.source << "to" << destination.absoluteFilePath();
- m_failedOperationType = Replace;
- m_failedFile = op.destination;
- fail();
- return;
- }
- QFile::setPermissions(destination.absoluteFilePath(), unixModeToPermissions(op.destinationMode));
- }
- break;
- // delete = move original to backup
- case GoUpdate::Operation::OP_DELETE:
- {
- QString destFilePath = FS::PathCombine(m_root, op.destination);
- if(QFile::exists(destFilePath))
- {
- QString backupName = op.destination;
- backupName.replace('/', '_');
- QString trashFilePath = FS::PathCombine(backupPath, backupName);
-
- if(!QFile::rename(destFilePath, trashFilePath))
- {
- qWarning() << "DELETE: Couldn't move:" << op.destination << "to" << trashFilePath;
- m_failedFile = op.destination;
- m_failedOperationType = Delete;
- fail();
- return;
- }
- BackupEntry be;
- be.original = destFilePath;
- be.backup = trashFilePath;
- m_delete_backups.append(be);
- }
- }
- break;
- }
- }
-
- // try to start the new binary
- args = qApp->arguments();
- args.removeFirst();
-
- // on old Windows, do insane things... no error checking here, this is just to have something.
- if(useXPHack)
- {
- QString script;
- auto nativePath = QDir::toNativeSeparators(exePath);
- auto nativeOriginPath = QDir::toNativeSeparators(exeOrigin);
- auto nativeBackupPath = QDir::toNativeSeparators(exeBackup);
-
- // so we write this vbscript thing...
- QTextStream out(&script);
- out << "WScript.Sleep 1000\n";
- out << "Set fso=CreateObject(\"Scripting.FileSystemObject\")\n";
- out << "Set shell=CreateObject(\"WScript.Shell\")\n";
- out << "fso.MoveFile \"" << nativePath << "\", \"" << nativeBackupPath << "\"\n";
- out << "fso.MoveFile \"" << nativeOriginPath << "\", \"" << nativePath << "\"\n";
- out << "shell.Run \"" << nativePath << "\"\n";
-
- QString scriptPath = FS::PathCombine(m_root, "update", "update.vbs");
-
- // we save it
- QFile scriptFile(scriptPath);
- scriptFile.open(QIODevice::WriteOnly);
- scriptFile.write(script.toLocal8Bit().replace("\n", "\r\n"));
- scriptFile.close();
-
- // we run it
- started = QProcess::startDetached("wscript", {scriptPath}, m_root);
-
- // and we quit. conscious thought.
- qApp->quit();
- return;
- }
- bool doLiveCheck = true;
- bool startFailed = false;
-
- // remove live check file, if any
- if(QFile::exists(liveCheckFile))
- {
- if(!QFile::remove(liveCheckFile))
- {
- qWarning() << "Couldn't remove the" << liveCheckFile << "file! We will proceed without :(";
- doLiveCheck = false;
- }
- }
-
- if(doLiveCheck)
- {
- if(!args.contains("--alive"))
- {
- args.append("--alive");
- }
- }
-
- // FIXME: reparse args and construct a safe variant from scratch. This is a workaround for GH-1874:
- QStringList realargs;
- int skip = 0;
- for(auto & arg: args)
- {
- if(skip)
- {
- skip--;
- continue;
- }
- if(arg == "-l")
- {
- skip = 1;
- continue;
- }
- realargs.append(arg);
- }
-
- // start the updated application
- started = QProcess::startDetached(finishCmd, realargs, QDir::currentPath(), &pid);
- // much dumber check - just find out if the call
- if(!started || pid == -1)
- {
- qWarning() << "Couldn't start new process properly!";
- startFailed = true;
- }
- if(!startFailed && doLiveCheck)
- {
- int attempts = 0;
- while(attempts < 10)
- {
- attempts++;
- QString key;
- std::this_thread::sleep_for(std::chrono::milliseconds(250));
- if(!QFile::exists(liveCheckFile))
- {
- qWarning() << "Couldn't find the" << liveCheckFile << "file!";
- startFailed = true;
- continue;
- }
- try
- {
- key = QString::fromUtf8(FS::read(liveCheckFile));
- auto id = ApplicationId::fromRawString(key);
- LocalPeer peer(nullptr, id);
- if(peer.isClient())
- {
- startFailed = false;
- qDebug() << "Found process started with key " << key;
- break;
- }
- else
- {
- startFailed = true;
- qDebug() << "Process started with key " << key << "apparently died or is not reponding...";
- break;
- }
- }
- catch (const Exception &e)
- {
- qWarning() << "Couldn't read the" << liveCheckFile << "file!";
- startFailed = true;
- continue;
- }
- }
- }
- if(startFailed)
- {
- m_failedOperationType = Start;
- fail();
- return;
- }
- else
- {
- origin.rmdir(m_updateFilesDir);
- qApp->quit();
- return;
- }
-}
-
-void UpdateController::fail()
-{
- qWarning() << "Update failed!";
-
- QString msg;
- bool doRollback = false;
- QString failTitle = QObject::tr("Update failed!");
- QString rollFailTitle = QObject::tr("Rollback failed!");
- switch (m_failedOperationType)
- {
- case Replace:
- {
- msg = QObject::tr(
- "Couldn't replace file %1. Changes will be reverted.\n"
- "See the %2 log file for details."
- ).arg(m_failedFile, BuildConfig.LAUNCHER_DISPLAYNAME);
- doRollback = true;
- QMessageBox::critical(m_parent, failTitle, msg);
- break;
- }
- case Delete:
- {
- msg = QObject::tr(
- "Couldn't remove file %1. Changes will be reverted.\n"
- "See the %2 log file for details."
- ).arg(m_failedFile, BuildConfig.LAUNCHER_DISPLAYNAME);
- doRollback = true;
- QMessageBox::critical(m_parent, failTitle, msg);
- break;
- }
- case Start:
- {
- msg = QObject::tr("The new version didn't start or is too old and doesn't respond to startup checks.\n"
- "\n"
- "Roll back to previous version?");
- auto result = QMessageBox::critical(
- m_parent,
- failTitle,
- msg,
- QMessageBox::Yes | QMessageBox::No,
- QMessageBox::Yes
- );
- doRollback = (result == QMessageBox::Yes);
- break;
- }
- case Nothing:
- default:
- return;
- }
- if(doRollback)
- {
- auto rollbackOK = rollback();
- if(!rollbackOK)
- {
- msg = QObject::tr("The rollback failed too.\n"
- "You will have to repair %1 manually.\n"
- "Please let us know why and how this happened.").arg(BuildConfig.LAUNCHER_DISPLAYNAME);
- QMessageBox::critical(m_parent, rollFailTitle, msg);
- qApp->quit();
- }
- }
- else
- {
- qApp->quit();
- }
-}
-
-bool UpdateController::rollback()
-{
- bool revertOK = true;
- // if the above failed, roll back changes
- for(auto backup:m_replace_backups)
- {
- qWarning() << "restoring" << backup.original << "from" << backup.backup;
- if(!QFile::rename(backup.original, backup.update))
- {
- revertOK = false;
- qWarning() << "moving new" << backup.original << "back to" << backup.update << "failed!";
- continue;
- }
-
- if(!QFile::rename(backup.backup, backup.original))
- {
- revertOK = false;
- qWarning() << "restoring" << backup.original << "failed!";
- }
- }
- for(auto backup:m_delete_backups)
- {
- qWarning() << "restoring" << backup.original << "from" << backup.backup;
- if(!QFile::rename(backup.backup, backup.original))
- {
- revertOK = false;
- qWarning() << "restoring" << backup.original << "failed!";
- }
- }
- return revertOK;
-}
diff --git a/launcher/UpdateController.h b/launcher/UpdateController.h
deleted file mode 100644
index 715554e5..00000000
--- a/launcher/UpdateController.h
+++ /dev/null
@@ -1,44 +0,0 @@
-#pragma once
-
-#include <QString>
-#include <QList>
-#include <updater/GoUpdate.h>
-
-class QWidget;
-
-class UpdateController
-{
-public:
- UpdateController(QWidget * parent, const QString &root, const QString updateFilesDir, GoUpdate::OperationList operations);
- void installUpdates();
-
-private:
- void fail();
- bool rollback();
-
-private:
- QString m_root;
- QString m_updateFilesDir;
- GoUpdate::OperationList m_operations;
- QWidget * m_parent;
-
- struct BackupEntry
- {
- // path where we got the new file from
- QString update;
- // path of what is being actually updated
- QString original;
- // path where the backup of the updated file was placed
- QString backup;
- };
- QList <BackupEntry> m_replace_backups;
- QList <BackupEntry> m_delete_backups;
- enum Failure
- {
- Replace,
- Delete,
- Start,
- Nothing
- } m_failedOperationType = Nothing;
- QString m_failedFile;
-};
diff --git a/launcher/java/JavaInstallList.cpp b/launcher/java/JavaInstallList.cpp
index e2f0aa00..b29af857 100644
--- a/launcher/java/JavaInstallList.cpp
+++ b/launcher/java/JavaInstallList.cpp
@@ -67,7 +67,7 @@ void JavaInstallList::load()
if(m_status != Status::InProgress)
{
m_status = Status::InProgress;
- m_loadTask = new JavaListLoadTask(this);
+ m_loadTask.reset(new JavaListLoadTask(this));
m_loadTask->start();
}
}
@@ -167,7 +167,7 @@ void JavaListLoadTask::executeTask()
JavaUtils ju;
QList<QString> candidate_paths = ju.FindJavaPaths();
- m_job = new JavaCheckerJob("Java detection");
+ m_job.reset(new JavaCheckerJob("Java detection"));
connect(m_job.get(), &Task::finished, this, &JavaListLoadTask::javaCheckerFinished);
connect(m_job.get(), &Task::progress, this, &Task::setProgress);
diff --git a/launcher/launch/steps/CheckJava.cpp b/launcher/launch/steps/CheckJava.cpp
index 7aeb61bf..f0187586 100644
--- a/launcher/launch/steps/CheckJava.cpp
+++ b/launcher/launch/steps/CheckJava.cpp
@@ -93,7 +93,7 @@ void CheckJava::executeTask()
|| storedArchitecture.size() == 0 || storedRealArchitecture.size() == 0
|| storedVendor.size() == 0)
{
- m_JavaChecker = new JavaChecker();
+ m_JavaChecker.reset(new JavaChecker);
emit logLine(QString("Checking Java version..."), MessageLevel::Launcher);
connect(m_JavaChecker.get(), &JavaChecker::checkFinished, this, &CheckJava::checkJavaFinished);
m_JavaChecker->m_path = realJavaPath;
diff --git a/launcher/meta/BaseEntity.cpp b/launcher/meta/BaseEntity.cpp
index de4e1012..97815eba 100644
--- a/launcher/meta/BaseEntity.cpp
+++ b/launcher/meta/BaseEntity.cpp
@@ -126,7 +126,7 @@ void Meta::BaseEntity::load(Net::Mode loadType)
{
return;
}
- m_updateTask = new NetJob(QObject::tr("Download of meta file %1").arg(localFilename()), APPLICATION->network());
+ m_updateTask.reset(new NetJob(QObject::tr("Download of meta file %1").arg(localFilename()), APPLICATION->network()));
auto url = this->url();
auto entry = APPLICATION->metacache()->resolveEntry("meta", localFilename());
entry->setStale(true);
diff --git a/launcher/minecraft/AssetsUtils.cpp b/launcher/minecraft/AssetsUtils.cpp
index 15062c2b..16fdfdb1 100644
--- a/launcher/minecraft/AssetsUtils.cpp
+++ b/launcher/minecraft/AssetsUtils.cpp
@@ -340,7 +340,7 @@ QString AssetObject::getRelPath()
NetJob::Ptr AssetsIndex::getDownloadJob()
{
- auto job = new NetJob(QObject::tr("Assets for %1").arg(id), APPLICATION->network());
+ auto job = makeShared<NetJob>(QObject::tr("Assets for %1").arg(id), APPLICATION->network());
for (auto &object : objects.values())
{
auto dl = object.getDownloadAction();
diff --git a/launcher/minecraft/ComponentUpdateTask.cpp b/launcher/minecraft/ComponentUpdateTask.cpp
index 6db21622..d55bc17f 100644
--- a/launcher/minecraft/ComponentUpdateTask.cpp
+++ b/launcher/minecraft/ComponentUpdateTask.cpp
@@ -572,7 +572,7 @@ void ComponentUpdateTask::resolveDependencies(bool checkOnly)
// add stuff...
for(auto &add: toAdd)
{
- ComponentPtr component = new Component(d->m_list, add.uid);
+ auto component = makeShared<Component>(d->m_list, add.uid);
if(!add.equalsVersion.isEmpty())
{
// exact version
diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp
index d0a5ed31..8a814cbf 100644
--- a/launcher/minecraft/MinecraftInstance.cpp
+++ b/launcher/minecraft/MinecraftInstance.cpp
@@ -962,12 +962,12 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt
// print a header
{
- process->appendStep(new TextPrint(pptr, "Minecraft folder is:\n" + gameRoot() + "\n\n", MessageLevel::Launcher));
+ process->appendStep(makeShared<TextPrint>(pptr, "Minecraft folder is:\n" + gameRoot() + "\n\n", MessageLevel::Launcher));
}
// check java
{
- process->appendStep(new CheckJava(pptr));
+ process->appendStep(makeShared<CheckJava>(pptr));
}
// check launch method
@@ -975,13 +975,13 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt
QString method = launchMethod();
if(!validMethods.contains(method))
{
- process->appendStep(new TextPrint(pptr, "Selected launch method \"" + method + "\" is not valid.\n", MessageLevel::Fatal));
+ process->appendStep(makeShared<TextPrint>(pptr, "Selected launch method \"" + method + "\" is not valid.\n", MessageLevel::Fatal));
return process;
}
// create the .minecraft folder and server-resource-packs (workaround for Minecraft bug MCL-3732)
{
- process->appendStep(new CreateGameFolders(pptr));
+ process->appendStep(makeShared<CreateGameFolders>(pptr));
}
if (!serverToJoin && settings()->get("JoinServerOnLaunch").toBool())
@@ -993,7 +993,7 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt
if(serverToJoin && serverToJoin->port == 25565)
{
// Resolve server address to join on launch
- auto *step = new LookupServerAddress(pptr);
+ auto step = makeShared<LookupServerAddress>(pptr);
step->setLookupAddress(serverToJoin->address);
step->setOutputAddressPtr(serverToJoin);
process->appendStep(step);
@@ -1002,7 +1002,7 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt
// run pre-launch command if that's needed
if(getPreLaunchCommand().size())
{
- auto step = new PreLaunchCommand(pptr);
+ auto step = makeShared<PreLaunchCommand>(pptr);
step->setWorkingDirectory(gameRoot());
process->appendStep(step);
}
@@ -1011,43 +1011,43 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt
if(session->status != AuthSession::PlayableOffline)
{
if(!session->demo) {
- process->appendStep(new ClaimAccount(pptr, session));
+ process->appendStep(makeShared<ClaimAccount>(pptr, session));
}
- process->appendStep(new Update(pptr, Net::Mode::Online));
+ process->appendStep(makeShared<Update>(pptr, Net::Mode::Online));
}
else
{
- process->appendStep(new Update(pptr, Net::Mode::Offline));
+ process->appendStep(makeShared<Update>(pptr, Net::Mode::Offline));
}
// if there are any jar mods
{
- process->appendStep(new ModMinecraftJar(pptr));
+ process->appendStep(makeShared<ModMinecraftJar>(pptr));
}
// Scan mods folders for mods
{
- process->appendStep(new ScanModFolders(pptr));
+ process->appendStep(makeShared<ScanModFolders>(pptr));
}
// print some instance info here...
{
- process->appendStep(new PrintInstanceInfo(pptr, session, serverToJoin));
+ process->appendStep(makeShared<PrintInstanceInfo>(pptr, session, serverToJoin));
}
// extract native jars if needed
{
- process->appendStep(new ExtractNatives(pptr));
+ process->appendStep(makeShared<ExtractNatives>(pptr));
}
// reconstruct assets if needed
{
- process->appendStep(new ReconstructAssets(pptr));
+ process->appendStep(makeShared<ReconstructAssets>(pptr));
}
// verify that minimum Java requirements are met
{
- process->appendStep(new VerifyJavaInstall(pptr));
+ process->appendStep(makeShared<VerifyJavaInstall>(pptr));
}
{
@@ -1055,7 +1055,7 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt
auto method = launchMethod();
if(method == "LauncherPart")
{
- auto step = new LauncherPartLaunch(pptr);
+ auto step = makeShared<LauncherPartLaunch>(pptr);
step->setWorkingDirectory(gameRoot());
step->setAuthSession(session);
step->setServerToJoin(serverToJoin);
@@ -1063,7 +1063,7 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt
}
else if (method == "DirectJava")
{
- auto step = new DirectJavaLaunch(pptr);
+ auto step = makeShared<DirectJavaLaunch>(pptr);
step->setWorkingDirectory(gameRoot());
step->setAuthSession(session);
step->setServerToJoin(serverToJoin);
@@ -1074,7 +1074,7 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt
// run post-exit command if that's needed
if(getPostExitCommand().size())
{
- auto step = new PostLaunchCommand(pptr);
+ auto step = makeShared<PostLaunchCommand>(pptr);
step->setWorkingDirectory(gameRoot());
process->appendStep(step);
}
@@ -1084,8 +1084,7 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt
}
if(m_settings->get("QuitAfterGameStop").toBool())
{
- auto step = new QuitAfterGameStop(pptr);
- process->appendStep(step);
+ process->appendStep(makeShared<QuitAfterGameStop>(pptr));
}
m_launchProcess = process;
emit launchTaskChanged(m_launchProcess);
diff --git a/launcher/minecraft/MinecraftUpdate.cpp b/launcher/minecraft/MinecraftUpdate.cpp
index 3a3aa864..07ad4882 100644
--- a/launcher/minecraft/MinecraftUpdate.cpp
+++ b/launcher/minecraft/MinecraftUpdate.cpp
@@ -43,7 +43,7 @@ void MinecraftUpdate::executeTask()
m_tasks.clear();
// create folders
{
- m_tasks.append(new FoldersTask(m_inst));
+ m_tasks.append(makeShared<FoldersTask>(m_inst));
}
// add metadata update task if necessary
@@ -59,17 +59,17 @@ void MinecraftUpdate::executeTask()
// libraries download
{
- m_tasks.append(new LibrariesTask(m_inst));
+ m_tasks.append(makeShared<LibrariesTask>(m_inst));
}
// FML libraries download and copy into the instance
{
- m_tasks.append(new FMLLibrariesTask(m_inst));
+ m_tasks.append(makeShared<FMLLibrariesTask>(m_inst));
}
// assets update
{
- m_tasks.append(new AssetUpdateTask(m_inst));
+ m_tasks.append(makeShared<AssetUpdateTask>(m_inst));
}
if(!m_preFailure.isEmpty())
diff --git a/launcher/minecraft/OneSixVersionFormat.cpp b/launcher/minecraft/OneSixVersionFormat.cpp
index 280f6b26..888b6860 100644
--- a/launcher/minecraft/OneSixVersionFormat.cpp
+++ b/launcher/minecraft/OneSixVersionFormat.cpp
@@ -39,6 +39,8 @@
#include "minecraft/ParseUtils.h"
#include <minecraft/MojangVersionFormat.h>
+#include <QRegularExpression>
+
using namespace Json;
static void readString(const QJsonObject &root, const QString &key, QString &variable)
@@ -121,6 +123,15 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument &doc
out->uid = root.value("fileId").toString();
}
+ const QRegularExpression valid_uid_regex{ QRegularExpression::anchoredPattern(QStringLiteral(R"([a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]+)*)")) };
+ if (!valid_uid_regex.match(out->uid).hasMatch()) {
+ qCritical() << "The component's 'uid' contains illegal characters! UID:" << out->uid;
+ out->addProblem(
+ ProblemSeverity::Error,
+ QObject::tr("The component's 'uid' contains illegal characters! This can cause security issues.")
+ );
+ }
+
out->version = root.value("version").toString();
MojangVersionFormat::readVersionProperties(root, out.get());
diff --git a/launcher/minecraft/PackProfile.cpp b/launcher/minecraft/PackProfile.cpp
index 43fa3f8d..da7c1d84 100644
--- a/launcher/minecraft/PackProfile.cpp
+++ b/launcher/minecraft/PackProfile.cpp
@@ -55,12 +55,13 @@
#include "PackProfile_p.h"
#include "ComponentUpdateTask.h"
-#include "modplatform/ModAPI.h"
+#include "Application.h"
+#include "modplatform/ResourceAPI.h"
-static const QMap<QString, ModAPI::ModLoaderType> modloaderMapping{
- {"net.minecraftforge", ModAPI::Forge},
- {"net.fabricmc.fabric-loader", ModAPI::Fabric},
- {"org.quiltmc.quilt-loader", ModAPI::Quilt}
+static const QMap<QString, ResourceAPI::ModLoaderType> modloaderMapping{
+ {"net.minecraftforge", ResourceAPI::Forge},
+ {"net.fabricmc.fabric-loader", ResourceAPI::Fabric},
+ {"org.quiltmc.quilt-loader", ResourceAPI::Quilt}
};
PackProfile::PackProfile(MinecraftInstance * instance)
@@ -129,7 +130,7 @@ static ComponentPtr componentFromJsonV1(PackProfile * parent, const QString & co
// critical
auto uid = Json::requireString(obj.value("uid"));
auto filePath = componentJsonPattern.arg(uid);
- auto component = new Component(parent, uid);
+ auto component = makeShared<Component>(parent, uid);
component->m_version = Json::ensureString(obj.value("version"));
component->m_dependencyOnly = Json::ensureBoolean(obj.value("dependencyOnly"), false);
component->m_important = Json::ensureBoolean(obj.value("important"), false);
@@ -517,23 +518,23 @@ bool PackProfile::revertToBase(int index)
return true;
}
-Component * PackProfile::getComponent(const QString &id)
+ComponentPtr PackProfile::getComponent(const QString &id)
{
auto iter = d->componentIndex.find(id);
if (iter == d->componentIndex.end())
{
return nullptr;
}
- return (*iter).get();
+ return (*iter);
}
-Component * PackProfile::getComponent(int index)
+ComponentPtr PackProfile::getComponent(int index)
{
if(index < 0 || index >= d->components.size())
{
return nullptr;
}
- return d->components[index].get();
+ return d->components[index];
}
QVariant PackProfile::data(const QModelIndex &index, int role) const
@@ -764,7 +765,7 @@ bool PackProfile::installEmpty(const QString& uid, const QString& name)
file.write(OneSixVersionFormat::versionFileToJson(f).toJson());
file.close();
- appendComponent(new Component(this, f->uid, f));
+ appendComponent(makeShared<Component>(this, f->uid, f));
scheduleSave();
invalidateLaunchProfile();
return true;
@@ -871,7 +872,7 @@ bool PackProfile::installJarMods_internal(QStringList filepaths)
file.write(OneSixVersionFormat::versionFileToJson(f).toJson());
file.close();
- appendComponent(new Component(this, f->uid, f));
+ appendComponent(makeShared<Component>(this, f->uid, f));
}
scheduleSave();
invalidateLaunchProfile();
@@ -932,7 +933,7 @@ bool PackProfile::installCustomJar_internal(QString filepath)
file.write(OneSixVersionFormat::versionFileToJson(f).toJson());
file.close();
- appendComponent(new Component(this, f->uid, f));
+ appendComponent(makeShared<Component>(this, f->uid, f));
scheduleSave();
invalidateLaunchProfile();
@@ -988,7 +989,7 @@ bool PackProfile::installAgents_internal(QStringList filepaths)
patchFile.write(OneSixVersionFormat::versionFileToJson(versionFile).toJson());
patchFile.close();
- appendComponent(new Component(this, versionFile->uid, versionFile));
+ appendComponent(makeShared<Component>(this, versionFile->uid, versionFile));
}
scheduleSave();
@@ -1037,7 +1038,7 @@ bool PackProfile::setComponentVersion(const QString& uid, const QString& version
else
{
// add new
- auto component = new Component(this, uid);
+ auto component = makeShared<Component>(this, uid);
component->m_version = version;
component->m_important = important;
appendComponent(component);
@@ -1066,19 +1067,22 @@ void PackProfile::disableInteraction(bool disable)
}
}
-ModAPI::ModLoaderTypes PackProfile::getModLoaders()
+std::optional<ResourceAPI::ModLoaderTypes> PackProfile::getModLoaders()
{
- ModAPI::ModLoaderTypes result = ModAPI::Unspecified;
+ ResourceAPI::ModLoaderTypes result;
+ bool has_any_loader = false;
- QMapIterator<QString, ModAPI::ModLoaderType> i(modloaderMapping);
+ QMapIterator<QString, ResourceAPI::ModLoaderType> i(modloaderMapping);
- while (i.hasNext())
- {
+ while (i.hasNext()) {
i.next();
- Component* c = getComponent(i.key());
- if (c != nullptr && c->isEnabled()) {
+ if (auto c = getComponent(i.key()); c != nullptr && c->isEnabled()) {
result |= i.value();
+ has_any_loader = true;
}
}
+
+ if (!has_any_loader)
+ return {};
return result;
}
diff --git a/launcher/minecraft/PackProfile.h b/launcher/minecraft/PackProfile.h
index 2330cca1..731cd0ba 100644
--- a/launcher/minecraft/PackProfile.h
+++ b/launcher/minecraft/PackProfile.h
@@ -49,7 +49,7 @@
#include "BaseVersion.h"
#include "MojangDownloadInfo.h"
#include "net/Mode.h"
-#include "modplatform/ModAPI.h"
+#include "modplatform/ResourceAPI.h"
class MinecraftInstance;
struct PackProfileData;
@@ -136,16 +136,16 @@ signals:
public:
/// get the profile component by id
- Component * getComponent(const QString &id);
+ ComponentPtr getComponent(const QString &id);
/// get the profile component by index
- Component * getComponent(int index);
+ ComponentPtr getComponent(int index);
/// Add the component to the internal list of patches
// todo(merged): is this the best approach
void appendComponent(ComponentPtr component);
- ModAPI::ModLoaderTypes getModLoaders();
+ std::optional<ResourceAPI::ModLoaderTypes> getModLoaders();
private:
void scheduleSave();
diff --git a/launcher/minecraft/auth/MinecraftAccount.cpp b/launcher/minecraft/auth/MinecraftAccount.cpp
index 73d570f1..48cf5d42 100644
--- a/launcher/minecraft/auth/MinecraftAccount.cpp
+++ b/launcher/minecraft/auth/MinecraftAccount.cpp
@@ -75,7 +75,7 @@ MinecraftAccountPtr MinecraftAccount::loadFromJsonV3(const QJsonObject& json) {
MinecraftAccountPtr MinecraftAccount::createFromUsername(const QString &username)
{
- MinecraftAccountPtr account = new MinecraftAccount();
+ auto account = makeShared<MinecraftAccount>();
account->data.type = AccountType::Mojang;
account->data.yggdrasilToken.extra["userName"] = username;
account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]"));
@@ -91,7 +91,7 @@ MinecraftAccountPtr MinecraftAccount::createBlankMSA()
MinecraftAccountPtr MinecraftAccount::createOffline(const QString &username)
{
- MinecraftAccountPtr account = new MinecraftAccount();
+ auto account = makeShared<MinecraftAccount>();
account->data.type = AccountType::Offline;
account->data.yggdrasilToken.token = "offline";
account->data.yggdrasilToken.validity = Katabasis::Validity::Certain;
diff --git a/launcher/minecraft/auth/flows/MSA.cpp b/launcher/minecraft/auth/flows/MSA.cpp
index 416b8f2c..f1987e0c 100644
--- a/launcher/minecraft/auth/flows/MSA.cpp
+++ b/launcher/minecraft/auth/flows/MSA.cpp
@@ -10,28 +10,28 @@
#include "minecraft/auth/steps/GetSkinStep.h"
MSASilent::MSASilent(AccountData* data, QObject* parent) : AuthFlow(data, parent) {
- m_steps.append(new MSAStep(m_data, MSAStep::Action::Refresh));
- m_steps.append(new XboxUserStep(m_data));
- m_steps.append(new XboxAuthorizationStep(m_data, &m_data->xboxApiToken, "http://xboxlive.com", "Xbox"));
- m_steps.append(new XboxAuthorizationStep(m_data, &m_data->mojangservicesToken, "rp://api.minecraftservices.com/", "Mojang"));
- m_steps.append(new LauncherLoginStep(m_data));
- m_steps.append(new XboxProfileStep(m_data));
- m_steps.append(new EntitlementsStep(m_data));
- m_steps.append(new MinecraftProfileStep(m_data));
- m_steps.append(new GetSkinStep(m_data));
+ m_steps.append(makeShared<MSAStep>(m_data, MSAStep::Action::Refresh));
+ m_steps.append(makeShared<XboxUserStep>(m_data));
+ m_steps.append(makeShared<XboxAuthorizationStep>(m_data, &m_data->xboxApiToken, "http://xboxlive.com", "Xbox"));
+ m_steps.append(makeShared<XboxAuthorizationStep>(m_data, &m_data->mojangservicesToken, "rp://api.minecraftservices.com/", "Mojang"));
+ m_steps.append(makeShared<LauncherLoginStep>(m_data));
+ m_steps.append(makeShared<XboxProfileStep>(m_data));
+ m_steps.append(makeShared<EntitlementsStep>(m_data));
+ m_steps.append(makeShared<MinecraftProfileStep>(m_data));
+ m_steps.append(makeShared<GetSkinStep>(m_data));
}
MSAInteractive::MSAInteractive(
AccountData* data,
QObject* parent
) : AuthFlow(data, parent) {
- m_steps.append(new MSAStep(m_data, MSAStep::Action::Login));
- m_steps.append(new XboxUserStep(m_data));
- m_steps.append(new XboxAuthorizationStep(m_data, &m_data->xboxApiToken, "http://xboxlive.com", "Xbox"));
- m_steps.append(new XboxAuthorizationStep(m_data, &m_data->mojangservicesToken, "rp://api.minecraftservices.com/", "Mojang"));
- m_steps.append(new LauncherLoginStep(m_data));
- m_steps.append(new XboxProfileStep(m_data));
- m_steps.append(new EntitlementsStep(m_data));
- m_steps.append(new MinecraftProfileStep(m_data));
- m_steps.append(new GetSkinStep(m_data));
+ m_steps.append(makeShared<MSAStep>(m_data, MSAStep::Action::Login));
+ m_steps.append(makeShared<XboxUserStep>(m_data));
+ m_steps.append(makeShared<XboxAuthorizationStep>(m_data, &m_data->xboxApiToken, "http://xboxlive.com", "Xbox"));
+ m_steps.append(makeShared<XboxAuthorizationStep>(m_data, &m_data->mojangservicesToken, "rp://api.minecraftservices.com/", "Mojang"));
+ m_steps.append(makeShared<LauncherLoginStep>(m_data));
+ m_steps.append(makeShared<XboxProfileStep>(m_data));
+ m_steps.append(makeShared<EntitlementsStep>(m_data));
+ m_steps.append(makeShared<MinecraftProfileStep>(m_data));
+ m_steps.append(makeShared<GetSkinStep>(m_data));
}
diff --git a/launcher/minecraft/auth/flows/Mojang.cpp b/launcher/minecraft/auth/flows/Mojang.cpp
index b86b0936..5900ea98 100644
--- a/launcher/minecraft/auth/flows/Mojang.cpp
+++ b/launcher/minecraft/auth/flows/Mojang.cpp
@@ -9,10 +9,10 @@ MojangRefresh::MojangRefresh(
AccountData *data,
QObject *parent
) : AuthFlow(data, parent) {
- m_steps.append(new YggdrasilStep(m_data, QString()));
- m_steps.append(new MinecraftProfileStepMojang(m_data));
- m_steps.append(new MigrationEligibilityStep(m_data));
- m_steps.append(new GetSkinStep(m_data));
+ m_steps.append(makeShared<YggdrasilStep>(m_data, QString()));
+ m_steps.append(makeShared<MinecraftProfileStepMojang>(m_data));
+ m_steps.append(makeShared<MigrationEligibilityStep>(m_data));
+ m_steps.append(makeShared<GetSkinStep>(m_data));
}
MojangLogin::MojangLogin(
@@ -20,8 +20,8 @@ MojangLogin::MojangLogin(
QString password,
QObject *parent
): AuthFlow(data, parent), m_password(password) {
- m_steps.append(new YggdrasilStep(m_data, m_password));
- m_steps.append(new MinecraftProfileStepMojang(m_data));
- m_steps.append(new MigrationEligibilityStep(m_data));
- m_steps.append(new GetSkinStep(m_data));
+ m_steps.append(makeShared<YggdrasilStep>(m_data, m_password));
+ m_steps.append(makeShared<MinecraftProfileStepMojang>(m_data));
+ m_steps.append(makeShared<MigrationEligibilityStep>(m_data));
+ m_steps.append(makeShared<GetSkinStep>(m_data));
}
diff --git a/launcher/minecraft/auth/flows/Offline.cpp b/launcher/minecraft/auth/flows/Offline.cpp
index fc614a8c..d5c63271 100644
--- a/launcher/minecraft/auth/flows/Offline.cpp
+++ b/launcher/minecraft/auth/flows/Offline.cpp
@@ -6,12 +6,12 @@ OfflineRefresh::OfflineRefresh(
AccountData *data,
QObject *parent
) : AuthFlow(data, parent) {
- m_steps.append(new OfflineStep(m_data));
+ m_steps.append(makeShared<OfflineStep>(m_data));
}
OfflineLogin::OfflineLogin(
AccountData *data,
QObject *parent
) : AuthFlow(data, parent) {
- m_steps.append(new OfflineStep(m_data));
+ m_steps.append(makeShared<OfflineStep>(m_data));
}
diff --git a/launcher/minecraft/launch/LauncherPartLaunch.cpp b/launcher/minecraft/launch/LauncherPartLaunch.cpp
index 1d8d7083..8ecf715d 100644
--- a/launcher/minecraft/launch/LauncherPartLaunch.cpp
+++ b/launcher/minecraft/launch/LauncherPartLaunch.cpp
@@ -36,6 +36,7 @@
#include "LauncherPartLaunch.h"
#include <QStandardPaths>
+#include <QRegularExpression>
#include "launch/LaunchTask.h"
#include "minecraft/MinecraftInstance.h"
diff --git a/launcher/minecraft/mod/ResourceFolderModel.cpp b/launcher/minecraft/mod/ResourceFolderModel.cpp
index a52c5db3..fdfb434b 100644
--- a/launcher/minecraft/mod/ResourceFolderModel.cpp
+++ b/launcher/minecraft/mod/ResourceFolderModel.cpp
@@ -260,7 +260,7 @@ void ResourceFolderModel::resolveResource(Resource* res)
return;
}
- auto task = createParseTask(*res);
+ Task::Ptr task{ createParseTask(*res) };
if (!task)
return;
@@ -270,11 +270,11 @@ void ResourceFolderModel::resolveResource(Resource* res)
m_active_parse_tasks.insert(ticket, task);
connect(
- task, &Task::succeeded, this, [=] { onParseSucceeded(ticket, res->internal_id()); }, Qt::ConnectionType::QueuedConnection);
+ task.get(), &Task::succeeded, this, [=] { onParseSucceeded(ticket, res->internal_id()); }, Qt::ConnectionType::QueuedConnection);
connect(
- task, &Task::failed, this, [=] { onParseFailed(ticket, res->internal_id()); }, Qt::ConnectionType::QueuedConnection);
+ task.get(), &Task::failed, this, [=] { onParseFailed(ticket, res->internal_id()); }, Qt::ConnectionType::QueuedConnection);
connect(
- task, &Task::finished, this, [=] { m_active_parse_tasks.remove(ticket); }, Qt::ConnectionType::QueuedConnection);
+ task.get(), &Task::finished, this, [=] { m_active_parse_tasks.remove(ticket); }, Qt::ConnectionType::QueuedConnection);
m_helper_thread_task.addTask(task);
diff --git a/launcher/minecraft/mod/ResourcePackFolderModel.cpp b/launcher/minecraft/mod/ResourcePackFolderModel.cpp
index ebac707d..da4bd091 100644
--- a/launcher/minecraft/mod/ResourcePackFolderModel.cpp
+++ b/launcher/minecraft/mod/ResourcePackFolderModel.cpp
@@ -142,7 +142,7 @@ int ResourcePackFolderModel::columnCount(const QModelIndex& parent) const
Task* ResourcePackFolderModel::createUpdateTask()
{
- return new BasicFolderLoadTask(m_dir, [](QFileInfo const& entry) { return new ResourcePack(entry); });
+ return new BasicFolderLoadTask(m_dir, [](QFileInfo const& entry) { return makeShared<ResourcePack>(entry); });
}
Task* ResourcePackFolderModel::createParseTask(Resource& resource)
diff --git a/launcher/minecraft/mod/TexturePackFolderModel.cpp b/launcher/minecraft/mod/TexturePackFolderModel.cpp
index 561f6202..5a32cfaf 100644
--- a/launcher/minecraft/mod/TexturePackFolderModel.cpp
+++ b/launcher/minecraft/mod/TexturePackFolderModel.cpp
@@ -43,7 +43,7 @@ TexturePackFolderModel::TexturePackFolderModel(const QString &dir) : ResourceFol
Task* TexturePackFolderModel::createUpdateTask()
{
- return new BasicFolderLoadTask(m_dir, [](QFileInfo const& entry) { return new TexturePack(entry); });
+ return new BasicFolderLoadTask(m_dir, [](QFileInfo const& entry) { return makeShared<TexturePack>(entry); });
}
Task* TexturePackFolderModel::createParseTask(Resource& resource)
diff --git a/launcher/minecraft/mod/tasks/BasicFolderLoadTask.h b/launcher/minecraft/mod/tasks/BasicFolderLoadTask.h
index 2fce2942..3ee7e2e0 100644
--- a/launcher/minecraft/mod/tasks/BasicFolderLoadTask.h
+++ b/launcher/minecraft/mod/tasks/BasicFolderLoadTask.h
@@ -26,11 +26,11 @@ class BasicFolderLoadTask : public Task {
public:
BasicFolderLoadTask(QDir dir) : Task(nullptr, false), m_dir(dir), m_result(new Result), m_thread_to_spawn_into(thread())
{
- m_create_func = [](QFileInfo const& entry) -> Resource* {
- return new Resource(entry);
+ m_create_func = [](QFileInfo const& entry) -> Resource::Ptr {
+ return makeShared<Resource>(entry);
};
}
- BasicFolderLoadTask(QDir dir, std::function<Resource*(QFileInfo const&)> create_function)
+ BasicFolderLoadTask(QDir dir, std::function<Resource::Ptr(QFileInfo const&)> create_function)
: Task(nullptr, false), m_dir(dir), m_result(new Result), m_create_func(std::move(create_function)), m_thread_to_spawn_into(thread())
{}
@@ -65,7 +65,7 @@ private:
std::atomic<bool> m_aborted = false;
- std::function<Resource*(QFileInfo const&)> m_create_func;
+ std::function<Resource::Ptr(QFileInfo const&)> m_create_func;
/** This is the thread in which we should put new mod objects */
QThread* m_thread_to_spawn_into;
diff --git a/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp
index 3fcb2110..5bb44877 100644
--- a/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp
+++ b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp
@@ -50,7 +50,7 @@ bool processFolder(DataPack& pack, ProcessingLevel level)
Q_ASSERT(pack.type() == ResourceType::FOLDER);
auto mcmeta_invalid = [&pack]() {
- qWarning() << "Resource pack at" << pack.fileinfo().filePath() << "does not have a valid pack.mcmeta";
+ qWarning() << "Data pack at" << pack.fileinfo().filePath() << "does not have a valid pack.mcmeta";
return false; // the mcmeta is not optional
};
@@ -95,7 +95,7 @@ bool processZIP(DataPack& pack, ProcessingLevel level)
QuaZipFile file(&zip);
auto mcmeta_invalid = [&pack]() {
- qWarning() << "Resource pack at" << pack.fileinfo().filePath() << "does not have a valid pack.mcmeta";
+ qWarning() << "Data pack at" << pack.fileinfo().filePath() << "does not have a valid pack.mcmeta";
return false; // the mcmeta is not optional
};
diff --git a/launcher/minecraft/mod/tasks/LocalResourceParse.cpp b/launcher/minecraft/mod/tasks/LocalResourceParse.cpp
index 19ddc899..4d760df2 100644
--- a/launcher/minecraft/mod/tasks/LocalResourceParse.cpp
+++ b/launcher/minecraft/mod/tasks/LocalResourceParse.cpp
@@ -19,6 +19,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+#include <QObject>
+
#include "LocalResourceParse.h"
#include "LocalDataPackParseTask.h"
@@ -28,6 +30,17 @@
#include "LocalTexturePackParseTask.h"
#include "LocalWorldSaveParseTask.h"
+
+static const QMap<PackedResourceType, QString> s_packed_type_names = {
+ {PackedResourceType::ResourcePack, QObject::tr("resource pack")},
+ {PackedResourceType::TexturePack, QObject::tr("texture pack")},
+ {PackedResourceType::DataPack, QObject::tr("data pack")},
+ {PackedResourceType::ShaderPack, QObject::tr("shader pack")},
+ {PackedResourceType::WorldSave, QObject::tr("world save")},
+ {PackedResourceType::Mod , QObject::tr("mod")},
+ {PackedResourceType::UNKNOWN, QObject::tr("unknown")}
+};
+
namespace ResourceUtils {
PackedResourceType identify(QFileInfo file){
if (file.exists() && file.isFile()) {
@@ -57,4 +70,9 @@ PackedResourceType identify(QFileInfo file){
}
return PackedResourceType::UNKNOWN;
}
+
+QString getPackedTypeName(PackedResourceType type) {
+ return s_packed_type_names.constFind(type).value();
+}
+
}
diff --git a/launcher/minecraft/mod/tasks/LocalResourceParse.h b/launcher/minecraft/mod/tasks/LocalResourceParse.h
index b07a874c..7385d24b 100644
--- a/launcher/minecraft/mod/tasks/LocalResourceParse.h
+++ b/launcher/minecraft/mod/tasks/LocalResourceParse.h
@@ -21,11 +21,17 @@
#pragma once
+#include <set>
+
#include <QDebug>
#include <QFileInfo>
#include <QObject>
enum class PackedResourceType { DataPack, ResourcePack, TexturePack, ShaderPack, WorldSave, Mod, UNKNOWN };
namespace ResourceUtils {
+static const std::set<PackedResourceType> ValidResourceTypes = { PackedResourceType::DataPack, PackedResourceType::ResourcePack,
+ PackedResourceType::TexturePack, PackedResourceType::ShaderPack,
+ PackedResourceType::WorldSave, PackedResourceType::Mod };
PackedResourceType identify(QFileInfo file);
+QString getPackedTypeName(PackedResourceType type);
} // namespace ResourceUtils
diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp
index 78ef4386..3677a1dc 100644
--- a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp
+++ b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp
@@ -72,14 +72,14 @@ void ModFolderLoadTask::executeTask()
delete mod;
}
else {
- m_result->mods[mod->internal_id()] = mod;
+ m_result->mods[mod->internal_id()].reset(std::move(mod));
m_result->mods[mod->internal_id()]->setStatus(ModStatus::NoMetadata);
}
}
else {
QString chopped_id = mod->internal_id().chopped(9);
if (m_result->mods.contains(chopped_id)) {
- m_result->mods[mod->internal_id()] = mod;
+ m_result->mods[mod->internal_id()].reset(std::move(mod));
auto metadata = m_result->mods[chopped_id]->metadata();
if (metadata) {
@@ -90,7 +90,7 @@ void ModFolderLoadTask::executeTask()
}
}
else {
- m_result->mods[mod->internal_id()] = mod;
+ m_result->mods[mod->internal_id()].reset(std::move(mod));
m_result->mods[mod->internal_id()]->setStatus(ModStatus::NoMetadata);
}
}
@@ -130,6 +130,6 @@ void ModFolderLoadTask::getFromMetadata()
auto* mod = new Mod(m_mods_dir, metadata);
mod->setStatus(ModStatus::NotInstalled);
- m_result->mods[mod->internal_id()] = mod;
+ m_result->mods[mod->internal_id()].reset(std::move(mod));
}
}
diff --git a/launcher/minecraft/update/AssetUpdateTask.cpp b/launcher/minecraft/update/AssetUpdateTask.cpp
index dd246665..8ccb0e1d 100644
--- a/launcher/minecraft/update/AssetUpdateTask.cpp
+++ b/launcher/minecraft/update/AssetUpdateTask.cpp
@@ -24,7 +24,7 @@ void AssetUpdateTask::executeTask()
auto assets = profile->getMinecraftAssets();
QUrl indexUrl = assets->url;
QString localPath = assets->id + ".json";
- auto job = new NetJob(
+ auto job = makeShared<NetJob>(
tr("Asset index for %1").arg(m_inst->name()),
APPLICATION->network()
);
diff --git a/launcher/minecraft/update/FMLLibrariesTask.cpp b/launcher/minecraft/update/FMLLibrariesTask.cpp
index 7a0bd2f3..96fd3ba3 100644
--- a/launcher/minecraft/update/FMLLibrariesTask.cpp
+++ b/launcher/minecraft/update/FMLLibrariesTask.cpp
@@ -61,7 +61,7 @@ void FMLLibrariesTask::executeTask()
// download missing libs to our place
setStatus(tr("Downloading FML libraries..."));
- auto dljob = new NetJob("FML libraries", APPLICATION->network());
+ NetJob::Ptr dljob{ new NetJob("FML libraries", APPLICATION->network()) };
auto metacache = APPLICATION->metacache();
Net::Download::Options options = Net::Download::Option::MakeEternal;
for (auto &lib : fmlLibsToProcess)
@@ -71,10 +71,10 @@ void FMLLibrariesTask::executeTask()
dljob->addNetAction(Net::Download::makeCached(QUrl(urlString), entry, options));
}
- connect(dljob, &NetJob::succeeded, this, &FMLLibrariesTask::fmllibsFinished);
- connect(dljob, &NetJob::failed, this, &FMLLibrariesTask::fmllibsFailed);
- connect(dljob, &NetJob::aborted, this, [this]{ emitFailed(tr("Aborted")); });
- connect(dljob, &NetJob::progress, this, &FMLLibrariesTask::progress);
+ connect(dljob.get(), &NetJob::succeeded, this, &FMLLibrariesTask::fmllibsFinished);
+ connect(dljob.get(), &NetJob::failed, this, &FMLLibrariesTask::fmllibsFailed);
+ connect(dljob.get(), &NetJob::aborted, this, [this]{ emitFailed(tr("Aborted")); });
+ connect(dljob.get(), &NetJob::progress, this, &FMLLibrariesTask::progress);
downloadJob.reset(dljob);
downloadJob->start();
}
diff --git a/launcher/minecraft/update/LibrariesTask.cpp b/launcher/minecraft/update/LibrariesTask.cpp
index 33a575c2..b9410111 100644
--- a/launcher/minecraft/update/LibrariesTask.cpp
+++ b/launcher/minecraft/update/LibrariesTask.cpp
@@ -20,7 +20,7 @@ void LibrariesTask::executeTask()
auto components = inst->getPackProfile();
auto profile = components->getProfile();
- auto job = new NetJob(tr("Libraries for instance %1").arg(inst->name()), APPLICATION->network());
+ NetJob::Ptr job{ new NetJob(tr("Libraries for instance %1").arg(inst->name()), APPLICATION->network()) };
downloadJob.reset(job);
auto metacache = APPLICATION->metacache();
diff --git a/launcher/modplatform/CheckUpdateTask.h b/launcher/modplatform/CheckUpdateTask.h
index 91922034..f7582b8f 100644
--- a/launcher/modplatform/CheckUpdateTask.h
+++ b/launcher/modplatform/CheckUpdateTask.h
@@ -1,18 +1,18 @@
#pragma once
#include "minecraft/mod/Mod.h"
-#include "modplatform/ModAPI.h"
+#include "modplatform/ResourceAPI.h"
#include "modplatform/ModIndex.h"
#include "tasks/Task.h"
-class ModDownloadTask;
+class ResourceDownloadTask;
class ModFolderModel;
class CheckUpdateTask : public Task {
Q_OBJECT
public:
- CheckUpdateTask(QList<Mod*>& mods, std::list<Version>& mcVersions, ModAPI::ModLoaderTypes loaders, std::shared_ptr<ModFolderModel> mods_folder)
+ CheckUpdateTask(QList<Mod*>& mods, std::list<Version>& mcVersions, std::optional<ResourceAPI::ModLoaderTypes> loaders, std::shared_ptr<ModFolderModel> mods_folder)
: Task(nullptr), m_mods(mods), m_game_versions(mcVersions), m_loaders(loaders), m_mods_folder(mods_folder) {};
struct UpdatableMod {
@@ -21,11 +21,11 @@ class CheckUpdateTask : public Task {
QString old_version;
QString new_version;
QString changelog;
- ModPlatform::Provider provider;
- ModDownloadTask* download;
+ ModPlatform::ResourceProvider provider;
+ shared_qobject_ptr<ResourceDownloadTask> download;
public:
- UpdatableMod(QString name, QString old_h, QString old_v, QString new_v, QString changelog, ModPlatform::Provider p, ModDownloadTask* t)
+ UpdatableMod(QString name, QString old_h, QString old_v, QString new_v, QString changelog, ModPlatform::ResourceProvider p, shared_qobject_ptr<ResourceDownloadTask> t)
: name(name), old_hash(old_h), old_version(old_v), new_version(new_v), changelog(changelog), provider(p), download(t)
{}
};
@@ -44,7 +44,7 @@ class CheckUpdateTask : public Task {
protected:
QList<Mod*>& m_mods;
std::list<Version>& m_game_versions;
- ModAPI::ModLoaderTypes m_loaders;
+ std::optional<ResourceAPI::ModLoaderTypes> m_loaders;
std::shared_ptr<ModFolderModel> m_mods_folder;
std::vector<UpdatableMod> m_updatable;
diff --git a/launcher/modplatform/EnsureMetadataTask.cpp b/launcher/modplatform/EnsureMetadataTask.cpp
index 234330a7..34d969f0 100644
--- a/launcher/modplatform/EnsureMetadataTask.cpp
+++ b/launcher/modplatform/EnsureMetadataTask.cpp
@@ -13,14 +13,12 @@
#include "modplatform/modrinth/ModrinthAPI.h"
#include "modplatform/modrinth/ModrinthPackIndex.h"
-#include "net/NetJob.h"
-
static ModPlatform::ProviderCapabilities ProviderCaps;
static ModrinthAPI modrinth_api;
static FlameAPI flame_api;
-EnsureMetadataTask::EnsureMetadataTask(Mod* mod, QDir dir, ModPlatform::Provider prov)
+EnsureMetadataTask::EnsureMetadataTask(Mod* mod, QDir dir, ModPlatform::ResourceProvider prov)
: Task(nullptr), m_index_dir(dir), m_provider(prov), m_hashing_task(nullptr), m_current_task(nullptr)
{
auto hash_task = createNewHash(mod);
@@ -31,10 +29,10 @@ EnsureMetadataTask::EnsureMetadataTask(Mod* mod, QDir dir, ModPlatform::Provider
hash_task->start();
}
-EnsureMetadataTask::EnsureMetadataTask(QList<Mod*>& mods, QDir dir, ModPlatform::Provider prov)
+EnsureMetadataTask::EnsureMetadataTask(QList<Mod*>& mods, QDir dir, ModPlatform::ResourceProvider prov)
: Task(nullptr), m_index_dir(dir), m_provider(prov), m_current_task(nullptr)
{
- m_hashing_task = new ConcurrentTask(this, "MakeHashesTask", 10);
+ m_hashing_task.reset(new ConcurrentTask(this, "MakeHashesTask", 10));
for (auto* mod : mods) {
auto hash_task = createNewHash(mod);
if (!hash_task)
@@ -107,13 +105,13 @@ void EnsureMetadataTask::executeTask()
}
}
- NetJob::Ptr version_task;
+ Task::Ptr version_task;
switch (m_provider) {
- case (ModPlatform::Provider::MODRINTH):
+ case (ModPlatform::ResourceProvider::MODRINTH):
version_task = modrinthVersionsTask();
break;
- case (ModPlatform::Provider::FLAME):
+ case (ModPlatform::ResourceProvider::FLAME):
version_task = flameVersionsTask();
break;
}
@@ -127,13 +125,13 @@ void EnsureMetadataTask::executeTask()
};
connect(version_task.get(), &Task::finished, this, [this, invalidade_leftover] {
- NetJob::Ptr project_task;
+ Task::Ptr project_task;
switch (m_provider) {
- case (ModPlatform::Provider::MODRINTH):
+ case (ModPlatform::ResourceProvider::MODRINTH):
project_task = modrinthProjectsTask();
break;
- case (ModPlatform::Provider::FLAME):
+ case (ModPlatform::ResourceProvider::FLAME):
project_task = flameProjectsTask();
break;
}
@@ -149,7 +147,7 @@ void EnsureMetadataTask::executeTask()
m_current_task = nullptr;
});
- m_current_task = project_task.get();
+ m_current_task = project_task;
project_task->start();
});
@@ -164,7 +162,7 @@ void EnsureMetadataTask::executeTask()
setStatus(tr("Requesting metadata information from %1 for '%2'...")
.arg(ProviderCaps.readableName(m_provider), m_mods.begin().value()->name()));
- m_current_task = version_task.get();
+ m_current_task = version_task;
version_task->start();
}
@@ -210,18 +208,18 @@ void EnsureMetadataTask::emitFail(Mod* m, QString key, RemoveFromList remove)
// Modrinth
-NetJob::Ptr EnsureMetadataTask::modrinthVersionsTask()
+Task::Ptr EnsureMetadataTask::modrinthVersionsTask()
{
- auto hash_type = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH).first();
+ auto hash_type = ProviderCaps.hashType(ModPlatform::ResourceProvider::MODRINTH).first();
auto* response = new QByteArray();
auto ver_task = modrinth_api.currentVersions(m_mods.keys(), hash_type, response);
// Prevents unfortunate timings when aborting the task
if (!ver_task)
- return {};
+ return Task::Ptr{nullptr};
- connect(ver_task.get(), &NetJob::succeeded, this, [this, response] {
+ connect(ver_task.get(), &Task::succeeded, this, [this, response] {
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
@@ -260,14 +258,14 @@ NetJob::Ptr EnsureMetadataTask::modrinthVersionsTask()
return ver_task;
}
-NetJob::Ptr EnsureMetadataTask::modrinthProjectsTask()
+Task::Ptr EnsureMetadataTask::modrinthProjectsTask()
{
QHash<QString, QString> addonIds;
for (auto const& data : m_temp_versions)
addonIds.insert(data.addonId.toString(), data.hash);
auto response = new QByteArray();
- NetJob::Ptr proj_task;
+ Task::Ptr proj_task;
if (addonIds.isEmpty()) {
qWarning() << "No addonId found!";
@@ -279,9 +277,9 @@ NetJob::Ptr EnsureMetadataTask::modrinthProjectsTask()
// Prevents unfortunate timings when aborting the task
if (!proj_task)
- return {};
+ return Task::Ptr{nullptr};
- connect(proj_task.get(), &NetJob::succeeded, this, [this, response, addonIds] {
+ connect(proj_task.get(), &Task::succeeded, this, [this, response, addonIds] {
QJsonParseError parse_error{};
auto doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
@@ -291,43 +289,53 @@ NetJob::Ptr EnsureMetadataTask::modrinthProjectsTask()
return;
}
+ QJsonArray entries;
+
try {
- QJsonArray entries;
if (addonIds.size() == 1)
entries = { doc.object() };
else
entries = Json::requireArray(doc);
+ } catch (Json::JsonException& e) {
+ qDebug() << e.cause();
+ qDebug() << doc;
+ }
- for (auto entry : entries) {
+ for (auto entry : entries) {
+ ModPlatform::IndexedPack pack;
+
+ try {
auto entry_obj = Json::requireObject(entry);
- ModPlatform::IndexedPack pack;
Modrinth::loadIndexedPack(pack, entry_obj);
+ } catch (Json::JsonException& e) {
+ qDebug() << e.cause();
+ qDebug() << doc;
- auto hash = addonIds.find(pack.addonId.toString()).value();
+ // Skip this entry, since it has problems
+ continue;
+ }
- auto mod_iter = m_mods.find(hash);
- if (mod_iter == m_mods.end()) {
- qWarning() << "Invalid project id from the API response.";
- continue;
- }
+ auto hash = addonIds.find(pack.addonId.toString()).value();
- auto* mod = mod_iter.value();
+ auto mod_iter = m_mods.find(hash);
+ if (mod_iter == m_mods.end()) {
+ qWarning() << "Invalid project id from the API response.";
+ continue;
+ }
- try {
- setStatus(tr("Parsing API response from Modrinth for '%1'...").arg(mod->name()));
+ auto* mod = mod_iter.value();
- modrinthCallback(pack, m_temp_versions.find(hash).value(), mod);
- } catch (Json::JsonException& e) {
- qDebug() << e.cause();
- qDebug() << entries;
+ try {
+ setStatus(tr("Parsing API response from Modrinth for '%1'...").arg(mod->name()));
- emitFail(mod);
- }
+ modrinthCallback(pack, m_temp_versions.find(hash).value(), mod);
+ } catch (Json::JsonException& e) {
+ qDebug() << e.cause();
+ qDebug() << entries;
+
+ emitFail(mod);
}
- } catch (Json::JsonException& e) {
- qDebug() << e.cause();
- qDebug() << doc;
}
});
@@ -335,7 +343,7 @@ NetJob::Ptr EnsureMetadataTask::modrinthProjectsTask()
}
// Flame
-NetJob::Ptr EnsureMetadataTask::flameVersionsTask()
+Task::Ptr EnsureMetadataTask::flameVersionsTask()
{
auto* response = new QByteArray();
@@ -400,7 +408,7 @@ NetJob::Ptr EnsureMetadataTask::flameVersionsTask()
return ver_task;
}
-NetJob::Ptr EnsureMetadataTask::flameProjectsTask()
+Task::Ptr EnsureMetadataTask::flameProjectsTask()
{
QHash<QString, QString> addonIds;
for (auto const& hash : m_mods.keys()) {
@@ -414,7 +422,7 @@ NetJob::Ptr EnsureMetadataTask::flameProjectsTask()
}
auto response = new QByteArray();
- NetJob::Ptr proj_task;
+ Task::Ptr proj_task;
if (addonIds.isEmpty()) {
qWarning() << "No addonId found!";
@@ -426,9 +434,9 @@ NetJob::Ptr EnsureMetadataTask::flameProjectsTask()
// Prevents unfortunate timings when aborting the task
if (!proj_task)
- return {};
+ return Task::Ptr{nullptr};
- connect(proj_task.get(), &NetJob::succeeded, this, [this, response, addonIds] {
+ connect(proj_task.get(), &Task::succeeded, this, [this, response, addonIds] {
QJsonParseError parse_error{};
auto doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
diff --git a/launcher/modplatform/EnsureMetadataTask.h b/launcher/modplatform/EnsureMetadataTask.h
index a8b0851e..03cae4e4 100644
--- a/launcher/modplatform/EnsureMetadataTask.h
+++ b/launcher/modplatform/EnsureMetadataTask.h
@@ -14,8 +14,8 @@ class EnsureMetadataTask : public Task {
Q_OBJECT
public:
- EnsureMetadataTask(Mod*, QDir, ModPlatform::Provider = ModPlatform::Provider::MODRINTH);
- EnsureMetadataTask(QList<Mod*>&, QDir, ModPlatform::Provider = ModPlatform::Provider::MODRINTH);
+ EnsureMetadataTask(Mod*, QDir, ModPlatform::ResourceProvider = ModPlatform::ResourceProvider::MODRINTH);
+ EnsureMetadataTask(QList<Mod*>&, QDir, ModPlatform::ResourceProvider = ModPlatform::ResourceProvider::MODRINTH);
~EnsureMetadataTask() = default;
@@ -28,11 +28,11 @@ class EnsureMetadataTask : public Task {
private:
// FIXME: Move to their own namespace
- auto modrinthVersionsTask() -> NetJob::Ptr;
- auto modrinthProjectsTask() -> NetJob::Ptr;
+ auto modrinthVersionsTask() -> Task::Ptr;
+ auto modrinthProjectsTask() -> Task::Ptr;
- auto flameVersionsTask() -> NetJob::Ptr;
- auto flameProjectsTask() -> NetJob::Ptr;
+ auto flameVersionsTask() -> Task::Ptr;
+ auto flameProjectsTask() -> Task::Ptr;
// Helpers
enum class RemoveFromList {
@@ -57,9 +57,9 @@ class EnsureMetadataTask : public Task {
private:
QHash<QString, Mod*> m_mods;
QDir m_index_dir;
- ModPlatform::Provider m_provider;
+ ModPlatform::ResourceProvider m_provider;
QHash<QString, ModPlatform::IndexedVersion> m_temp_versions;
- ConcurrentTask* m_hashing_task;
- NetJob* m_current_task;
+ ConcurrentTask::Ptr m_hashing_task;
+ Task::Ptr m_current_task;
};
diff --git a/launcher/modplatform/ModAPI.h b/launcher/modplatform/ModAPI.h
deleted file mode 100644
index 703de143..00000000
--- a/launcher/modplatform/ModAPI.h
+++ /dev/null
@@ -1,118 +0,0 @@
-// SPDX-License-Identifier: GPL-3.0-only
-/*
- * PolyMC - Minecraft Launcher
- * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- *
- * This file incorporates work covered by the following copyright and
- * permission notice:
- *
- * Copyright 2013-2021 MultiMC Contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#pragma once
-
-#include <QString>
-#include <QList>
-#include <list>
-
-#include "../Version.h"
-#include "net/NetJob.h"
-
-namespace ModPlatform {
-class ListModel;
-struct IndexedPack;
-}
-
-class ModAPI {
- protected:
- using CallerType = ModPlatform::ListModel;
-
- public:
- virtual ~ModAPI() = default;
-
- enum ModLoaderType {
- Unspecified = 0,
- Forge = 1 << 0,
- Cauldron = 1 << 1,
- LiteLoader = 1 << 2,
- Fabric = 1 << 3,
- Quilt = 1 << 4
- };
- Q_DECLARE_FLAGS(ModLoaderTypes, ModLoaderType)
-
- struct SearchArgs {
- int offset;
- QString search;
- QString sorting;
- ModLoaderTypes loaders;
- std::list<Version> versions;
- };
-
- virtual void searchMods(CallerType* caller, SearchArgs&& args) const = 0;
- virtual void getModInfo(ModPlatform::IndexedPack& pack, std::function<void(QJsonDocument&, ModPlatform::IndexedPack&)> callback) = 0;
-
- virtual auto getProject(QString addonId, QByteArray* response) const -> NetJob* = 0;
- virtual auto getProjects(QStringList addonIds, QByteArray* response) const -> NetJob* = 0;
-
-
- struct VersionSearchArgs {
- QString addonId;
- std::list<Version> mcVersions;
- ModLoaderTypes loaders;
- };
-
- virtual void getVersions(VersionSearchArgs&& args, std::function<void(QJsonDocument&, QString)> callback) const = 0;
-
- static auto getModLoaderString(ModLoaderType type) -> const QString {
- switch (type) {
- case Unspecified:
- break;
- case Forge:
- return "forge";
- case Cauldron:
- return "cauldron";
- case LiteLoader:
- return "liteloader";
- case Fabric:
- return "fabric";
- case Quilt:
- return "quilt";
- }
- return "";
- }
-
- protected:
- inline auto getGameVersionsString(std::list<Version> mcVersions) const -> QString
- {
- QString s;
- for(auto& ver : mcVersions){
- s += QString("\"%1\",").arg(ver.toString());
- }
- s.remove(s.length() - 1, 1); //remove last comma
- return s;
- }
-};
diff --git a/launcher/modplatform/ModIndex.cpp b/launcher/modplatform/ModIndex.cpp
index 34fd9f30..6a507caf 100644
--- a/launcher/modplatform/ModIndex.cpp
+++ b/launcher/modplatform/ModIndex.cpp
@@ -24,47 +24,47 @@
namespace ModPlatform {
-auto ProviderCapabilities::name(Provider p) -> const char*
+auto ProviderCapabilities::name(ResourceProvider p) -> const char*
{
switch (p) {
- case Provider::MODRINTH:
+ case ResourceProvider::MODRINTH:
return "modrinth";
- case Provider::FLAME:
+ case ResourceProvider::FLAME:
return "curseforge";
}
return {};
}
-auto ProviderCapabilities::readableName(Provider p) -> QString
+auto ProviderCapabilities::readableName(ResourceProvider p) -> QString
{
switch (p) {
- case Provider::MODRINTH:
+ case ResourceProvider::MODRINTH:
return "Modrinth";
- case Provider::FLAME:
+ case ResourceProvider::FLAME:
return "CurseForge";
}
return {};
}
-auto ProviderCapabilities::hashType(Provider p) -> QStringList
+auto ProviderCapabilities::hashType(ResourceProvider p) -> QStringList
{
switch (p) {
- case Provider::MODRINTH:
+ case ResourceProvider::MODRINTH:
return { "sha512", "sha1" };
- case Provider::FLAME:
+ case ResourceProvider::FLAME:
// Try newer formats first, fall back to old format
return { "sha1", "md5", "murmur2" };
}
return {};
}
-auto ProviderCapabilities::hash(Provider p, QIODevice* device, QString type) -> QString
+auto ProviderCapabilities::hash(ResourceProvider p, QIODevice* device, QString type) -> QString
{
QCryptographicHash::Algorithm algo = QCryptographicHash::Sha1;
switch (p) {
- case Provider::MODRINTH: {
+ case ResourceProvider::MODRINTH: {
algo = (type == "sha1") ? QCryptographicHash::Sha1 : QCryptographicHash::Sha512;
break;
}
- case Provider::FLAME:
+ case ResourceProvider::FLAME:
algo = (type == "sha1") ? QCryptographicHash::Sha1 : QCryptographicHash::Md5;
break;
}
diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h
index 518fed7c..b1f8050d 100644
--- a/launcher/modplatform/ModIndex.h
+++ b/launcher/modplatform/ModIndex.h
@@ -28,17 +28,16 @@ class QIODevice;
namespace ModPlatform {
-enum class Provider {
- MODRINTH,
- FLAME
-};
+enum class ResourceProvider { MODRINTH, FLAME };
+
+enum class ResourceType { MOD, RESOURCE_PACK };
class ProviderCapabilities {
public:
- auto name(Provider) -> const char*;
- auto readableName(Provider) -> QString;
- auto hashType(Provider) -> QStringList;
- auto hash(Provider, QIODevice*, QString type = "") -> QString;
+ auto name(ResourceProvider) -> const char*;
+ auto readableName(ResourceProvider) -> QString;
+ auto hashType(ResourceProvider) -> QStringList;
+ auto hash(ResourceProvider, QIODevice*, QString type = "") -> QString;
};
struct ModpackAuthor {
@@ -66,6 +65,10 @@ struct IndexedVersion {
QString hash;
bool is_preferred = true;
QString changelog;
+
+ // For internal use, not provided by APIs
+ bool is_currently_selected = false;
+ QString custom_target_folder;
};
struct ExtraPackData {
@@ -81,7 +84,7 @@ struct ExtraPackData {
struct IndexedPack {
QVariant addonId;
- Provider provider;
+ ResourceProvider provider;
QString name;
QString slug;
QString description;
@@ -96,9 +99,26 @@ struct IndexedPack {
// Don't load by default, since some modplatform don't have that info
bool extraDataLoaded = true;
ExtraPackData extraData;
+
+ // For internal use, not provided by APIs
+ [[nodiscard]] bool isVersionSelected(size_t index) const
+ {
+ if (!versionsLoaded)
+ return false;
+
+ return versions.at(index).is_currently_selected;
+ }
+ [[nodiscard]] bool isAnyVersionSelected() const
+ {
+ if (!versionsLoaded)
+ return false;
+
+ return std::any_of(versions.constBegin(), versions.constEnd(),
+ [](auto const& v) { return v.is_currently_selected; });
+ }
};
} // namespace ModPlatform
Q_DECLARE_METATYPE(ModPlatform::IndexedPack)
-Q_DECLARE_METATYPE(ModPlatform::Provider)
+Q_DECLARE_METATYPE(ModPlatform::ResourceProvider)
diff --git a/launcher/modplatform/ResourceAPI.h b/launcher/modplatform/ResourceAPI.h
new file mode 100644
index 00000000..34f33779
--- /dev/null
+++ b/launcher/modplatform/ResourceAPI.h
@@ -0,0 +1,177 @@
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
+/*
+ * PolyMC - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QDebug>
+#include <QList>
+#include <QString>
+
+#include <list>
+#include <optional>
+
+#include "../Version.h"
+
+#include "modplatform/ModIndex.h"
+#include "tasks/Task.h"
+
+/* Simple class with a common interface for interacting with APIs */
+class ResourceAPI {
+ public:
+ virtual ~ResourceAPI() = default;
+
+ enum ModLoaderType { Forge = 1 << 0, Cauldron = 1 << 1, LiteLoader = 1 << 2, Fabric = 1 << 3, Quilt = 1 << 4 };
+ Q_DECLARE_FLAGS(ModLoaderTypes, ModLoaderType)
+
+ struct SortingMethod {
+ // The index of the sorting method. Used to allow for arbitrary ordering in the list of methods.
+ // Used by Flame in the API request.
+ unsigned int index;
+ // The real name of the sorting, as used in the respective API specification.
+ // Used by Modrinth in the API request.
+ QString name;
+ // The human-readable name of the sorting, used for display in the UI.
+ QString readable_name;
+ };
+
+ struct SearchArgs {
+ ModPlatform::ResourceType type{};
+ int offset = 0;
+
+ std::optional<QString> search;
+ std::optional<SortingMethod> sorting;
+ std::optional<ModLoaderTypes> loaders;
+ std::optional<std::list<Version> > versions;
+ };
+ struct SearchCallbacks {
+ std::function<void(QJsonDocument&)> on_succeed;
+ std::function<void(QString const& reason, int network_error_code)> on_fail;
+ std::function<void()> on_abort;
+ };
+
+ struct VersionSearchArgs {
+ ModPlatform::IndexedPack pack;
+
+ std::optional<std::list<Version> > mcVersions;
+ std::optional<ModLoaderTypes> loaders;
+
+ VersionSearchArgs(VersionSearchArgs const&) = default;
+ void operator=(VersionSearchArgs other)
+ {
+ pack = other.pack;
+ mcVersions = other.mcVersions;
+ loaders = other.loaders;
+ }
+ };
+ struct VersionSearchCallbacks {
+ std::function<void(QJsonDocument&, ModPlatform::IndexedPack)> on_succeed;
+ };
+
+ struct ProjectInfoArgs {
+ ModPlatform::IndexedPack pack;
+
+ ProjectInfoArgs(ProjectInfoArgs const&) = default;
+ void operator=(ProjectInfoArgs other) { pack = other.pack; }
+ };
+ struct ProjectInfoCallbacks {
+ std::function<void(QJsonDocument&, ModPlatform::IndexedPack)> on_succeed;
+ };
+
+ public:
+ /** Gets a list of available sorting methods for this API. */
+ [[nodiscard]] virtual auto getSortingMethods() const -> QList<SortingMethod> = 0;
+
+ public slots:
+ [[nodiscard]] virtual Task::Ptr searchProjects(SearchArgs&&, SearchCallbacks&&) const
+ {
+ qWarning() << "TODO";
+ return nullptr;
+ }
+ [[nodiscard]] virtual Task::Ptr getProject(QString addonId, QByteArray* response) const
+ {
+ qWarning() << "TODO";
+ return nullptr;
+ }
+ [[nodiscard]] virtual Task::Ptr getProjects(QStringList addonIds, QByteArray* response) const
+ {
+ qWarning() << "TODO";
+ return nullptr;
+ }
+
+ [[nodiscard]] virtual Task::Ptr getProjectInfo(ProjectInfoArgs&&, ProjectInfoCallbacks&&) const
+ {
+ qWarning() << "TODO";
+ return nullptr;
+ }
+ [[nodiscard]] virtual Task::Ptr getProjectVersions(VersionSearchArgs&&, VersionSearchCallbacks&&) const
+ {
+ qWarning() << "TODO";
+ return nullptr;
+ }
+
+ static auto getModLoaderString(ModLoaderType type) -> const QString
+ {
+ switch (type) {
+ case Forge:
+ return "forge";
+ case Cauldron:
+ return "cauldron";
+ case LiteLoader:
+ return "liteloader";
+ case Fabric:
+ return "fabric";
+ case Quilt:
+ return "quilt";
+ default:
+ break;
+ }
+ return "";
+ }
+
+ protected:
+ [[nodiscard]] inline QString debugName() const { return "External resource API"; }
+
+ [[nodiscard]] inline auto getGameVersionsString(std::list<Version> mcVersions) const -> QString
+ {
+ QString s;
+ for (auto& ver : mcVersions) {
+ s += QString("\"%1\",").arg(ver.toString());
+ }
+ s.remove(s.length() - 1, 1); // remove last comma
+ return s;
+ }
+};
diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp
index 291ad916..4bd8b7f2 100644
--- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp
+++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp
@@ -81,16 +81,17 @@ bool PackInstallTask::abort()
void PackInstallTask::executeTask()
{
qDebug() << "PackInstallTask::executeTask: " << QThread::currentThreadId();
- auto *netJob = new NetJob("ATLauncher::VersionFetch", APPLICATION->network());
+ NetJob::Ptr netJob{ new NetJob("ATLauncher::VersionFetch", APPLICATION->network()) };
auto searchUrl = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "packs/%1/versions/%2/Configs.json")
.arg(m_pack_safe_name).arg(m_version_name);
netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response));
+
+ QObject::connect(netJob.get(), &NetJob::succeeded, this, &PackInstallTask::onDownloadSucceeded);
+ QObject::connect(netJob.get(), &NetJob::failed, this, &PackInstallTask::onDownloadFailed);
+ QObject::connect(netJob.get(), &NetJob::aborted, this, &PackInstallTask::onDownloadAborted);
+
jobPtr = netJob;
jobPtr->start();
-
- QObject::connect(netJob, &NetJob::succeeded, this, &PackInstallTask::onDownloadSucceeded);
- QObject::connect(netJob, &NetJob::failed, this, &PackInstallTask::onDownloadFailed);
- QObject::connect(netJob, &NetJob::aborted, this, &PackInstallTask::onDownloadAborted);
}
void PackInstallTask::onDownloadSucceeded()
@@ -552,7 +553,7 @@ bool PackInstallTask::createLibrariesComponent(QString instanceRoot, std::shared
file.write(OneSixVersionFormat::versionFileToJson(f).toJson());
file.close();
- profile->appendComponent(new Component(profile.get(), target_id, f));
+ profile->appendComponent(ComponentPtr{ new Component(profile.get(), target_id, f) });
return true;
}
@@ -641,7 +642,7 @@ bool PackInstallTask::createPackComponent(QString instanceRoot, std::shared_ptr<
file.write(OneSixVersionFormat::versionFileToJson(f).toJson());
file.close();
- profile->appendComponent(new Component(profile.get(), target_id, f));
+ profile->appendComponent(ComponentPtr{ new Component(profile.get(), target_id, f) });
return true;
}
@@ -649,7 +650,7 @@ void PackInstallTask::installConfigs()
{
qDebug() << "PackInstallTask::installConfigs: " << QThread::currentThreadId();
setStatus(tr("Downloading configs..."));
- jobPtr = new NetJob(tr("Config download"), APPLICATION->network());
+ jobPtr.reset(new NetJob(tr("Config download"), APPLICATION->network()));
auto path = QString("Configs/%1/%2.zip").arg(m_pack_safe_name).arg(m_version_name);
auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "packs/%1/versions/%2/Configs.zip")
@@ -747,7 +748,7 @@ void PackInstallTask::downloadMods()
setStatus(tr("Downloading mods..."));
jarmods.clear();
- jobPtr = new NetJob(tr("Mod download"), APPLICATION->network());
+ jobPtr.reset(new NetJob(tr("Mod download"), APPLICATION->network()));
for(const auto& mod : m_version.mods) {
// skip non-client mods
if(!mod.client) continue;
diff --git a/launcher/modplatform/flame/FileResolvingTask.cpp b/launcher/modplatform/flame/FileResolvingTask.cpp
index 7f1beb1a..d3a737bb 100644
--- a/launcher/modplatform/flame/FileResolvingTask.cpp
+++ b/launcher/modplatform/flame/FileResolvingTask.cpp
@@ -23,7 +23,7 @@ void Flame::FileResolvingTask::executeTask()
{
setStatus(tr("Resolving mod IDs..."));
setProgress(0, 3);
- m_dljob = new NetJob("Mod id resolver", m_network);
+ m_dljob.reset(new NetJob("Mod id resolver", m_network));
result.reset(new QByteArray());
//build json data to send
QJsonObject object;
@@ -43,7 +43,7 @@ void Flame::FileResolvingTask::netJobFinished()
{
setProgress(1, 3);
// job to check modrinth for blocked projects
- m_checkJob = new NetJob("Modrinth check", m_network);
+ m_checkJob.reset(new NetJob("Modrinth check", m_network));
blockedProjects = QMap<File *,QByteArray *>();
QJsonDocument doc;
diff --git a/launcher/modplatform/flame/FlameAPI.cpp b/launcher/modplatform/flame/FlameAPI.cpp
index 4d71da21..5ef9a409 100644
--- a/launcher/modplatform/flame/FlameAPI.cpp
+++ b/launcher/modplatform/flame/FlameAPI.cpp
@@ -1,15 +1,19 @@
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
#include "FlameAPI.h"
#include "FlameModIndex.h"
#include "Application.h"
#include "BuildConfig.h"
#include "Json.h"
-
+#include "net/NetJob.h"
#include "net/Upload.h"
-auto FlameAPI::matchFingerprints(const QList<uint>& fingerprints, QByteArray* response) -> NetJob::Ptr
+Task::Ptr FlameAPI::matchFingerprints(const QList<uint>& fingerprints, QByteArray* response)
{
- auto* netJob = new NetJob(QString("Flame::MatchFingerprints"), APPLICATION->network());
+ auto netJob = makeShared<NetJob>(QString("Flame::MatchFingerprints"), APPLICATION->network());
QJsonObject body_obj;
QJsonArray fingerprints_arr;
@@ -24,7 +28,7 @@ auto FlameAPI::matchFingerprints(const QList<uint>& fingerprints, QByteArray* re
netJob->addNetAction(Net::Upload::makeByteArray(QString("https://api.curseforge.com/v1/fingerprints"), response, body_raw));
- QObject::connect(netJob, &NetJob::finished, [response] { delete response; });
+ QObject::connect(netJob.get(), &NetJob::finished, [response] { delete response; });
return netJob;
}
@@ -106,13 +110,19 @@ auto FlameAPI::getModDescription(int modId) -> QString
auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::IndexedVersion
{
+ auto versions_url_optional = getVersionsURL(args);
+ if (!versions_url_optional.has_value())
+ return {};
+
+ auto versions_url = versions_url_optional.value();
+
QEventLoop loop;
- auto netJob = new NetJob(QString("Flame::GetLatestVersion(%1)").arg(args.addonId), APPLICATION->network());
+ auto netJob = new NetJob(QString("Flame::GetLatestVersion(%1)").arg(args.pack.name), APPLICATION->network());
auto response = new QByteArray();
ModPlatform::IndexedVersion ver;
- netJob->addNetAction(Net::Download::makeByteArray(getVersionsURL(args), response));
+ netJob->addNetAction(Net::Download::makeByteArray(versions_url, response));
QObject::connect(netJob, &NetJob::succeeded, [response, args, &ver] {
QJsonParseError parse_error{};
@@ -161,9 +171,9 @@ auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::Indexe
return ver;
}
-auto FlameAPI::getProjects(QStringList addonIds, QByteArray* response) const -> NetJob*
+Task::Ptr FlameAPI::getProjects(QStringList addonIds, QByteArray* response) const
{
- auto* netJob = new NetJob(QString("Flame::GetProjects"), APPLICATION->network());
+ auto netJob = makeShared<NetJob>(QString("Flame::GetProjects"), APPLICATION->network());
QJsonObject body_obj;
QJsonArray addons_arr;
@@ -178,15 +188,15 @@ auto FlameAPI::getProjects(QStringList addonIds, QByteArray* response) const ->
netJob->addNetAction(Net::Upload::makeByteArray(QString("https://api.curseforge.com/v1/mods"), response, body_raw));
- QObject::connect(netJob, &NetJob::finished, [response, netJob] { delete response; netJob->deleteLater(); });
- QObject::connect(netJob, &NetJob::failed, [body_raw] { qDebug() << body_raw; });
+ QObject::connect(netJob.get(), &NetJob::finished, [response] { delete response; });
+ QObject::connect(netJob.get(), &NetJob::failed, [body_raw] { qDebug() << body_raw; });
return netJob;
}
-auto FlameAPI::getFiles(const QStringList& fileIds, QByteArray* response) const -> NetJob*
+Task::Ptr FlameAPI::getFiles(const QStringList& fileIds, QByteArray* response) const
{
- auto* netJob = new NetJob(QString("Flame::GetFiles"), APPLICATION->network());
+ auto netJob = makeShared<NetJob>(QString("Flame::GetFiles"), APPLICATION->network());
QJsonObject body_obj;
QJsonArray files_arr;
@@ -201,8 +211,23 @@ auto FlameAPI::getFiles(const QStringList& fileIds, QByteArray* response) const
netJob->addNetAction(Net::Upload::makeByteArray(QString("https://api.curseforge.com/v1/mods/files"), response, body_raw));
- QObject::connect(netJob, &NetJob::finished, [response, netJob] { delete response; netJob->deleteLater(); });
- QObject::connect(netJob, &NetJob::failed, [body_raw] { qDebug() << body_raw; });
+ QObject::connect(netJob.get(), &NetJob::finished, [response] { delete response; });
+ QObject::connect(netJob.get(), &NetJob::failed, [body_raw] { qDebug() << body_raw; });
return netJob;
}
+
+// https://docs.curseforge.com/?python#tocS_ModsSearchSortField
+static QList<ResourceAPI::SortingMethod> s_sorts = { { 1, "Featured", QObject::tr("Sort by Featured") },
+ { 2, "Popularity", QObject::tr("Sort by Popularity") },
+ { 3, "LastUpdated", QObject::tr("Sort by Last Updated") },
+ { 4, "Name", QObject::tr("Sort by Name") },
+ { 5, "Author", QObject::tr("Sort by Author") },
+ { 6, "TotalDownloads", QObject::tr("Sort by Downloads") },
+ { 7, "Category", QObject::tr("Sort by Category") },
+ { 8, "GameVersion", QObject::tr("Sort by Game Version") } };
+
+QList<ResourceAPI::SortingMethod> FlameAPI::getSortingMethods() const
+{
+ return s_sorts;
+}
diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h
index 4c6ca64c..06d749e6 100644
--- a/launcher/modplatform/flame/FlameAPI.h
+++ b/launcher/modplatform/flame/FlameAPI.h
@@ -1,84 +1,84 @@
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
#pragma once
#include "modplatform/ModIndex.h"
-#include "modplatform/helpers/NetworkModAPI.h"
+#include "modplatform/helpers/NetworkResourceAPI.h"
-class FlameAPI : public NetworkModAPI {
+class FlameAPI : public NetworkResourceAPI {
public:
- auto matchFingerprints(const QList<uint>& fingerprints, QByteArray* response) -> NetJob::Ptr;
auto getModFileChangelog(int modId, int fileId) -> QString;
auto getModDescription(int modId) -> QString;
auto getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::IndexedVersion;
- auto getProjects(QStringList addonIds, QByteArray* response) const -> NetJob* override;
- auto getFiles(const QStringList& fileIds, QByteArray* response) const -> NetJob*;
+ Task::Ptr getProjects(QStringList addonIds, QByteArray* response) const override;
+ Task::Ptr matchFingerprints(const QList<uint>& fingerprints, QByteArray* response);
+ Task::Ptr getFiles(const QStringList& fileIds, QByteArray* response) const;
+
+ [[nodiscard]] auto getSortingMethods() const -> QList<ResourceAPI::SortingMethod> override;
private:
- inline auto getSortFieldInt(QString sortString) const -> int
+ static int getClassId(ModPlatform::ResourceType type)
{
- return sortString == "Featured" ? 1
- : sortString == "Popularity" ? 2
- : sortString == "LastUpdated" ? 3
- : sortString == "Name" ? 4
- : sortString == "Author" ? 5
- : sortString == "TotalDownloads" ? 6
- : sortString == "Category" ? 7
- : sortString == "GameVersion" ? 8
- : 1;
+ switch (type) {
+ default:
+ case ModPlatform::ResourceType::MOD:
+ return 6;
+ }
+ }
+
+ static int getMappedModLoader(ModLoaderTypes loaders)
+ {
+ // https://docs.curseforge.com/?http#tocS_ModLoaderType
+ if (loaders & Forge)
+ return 1;
+ if (loaders & Fabric)
+ return 4;
+ // TODO: remove this once Quilt drops official Fabric support
+ if (loaders & Quilt) // NOTE: Most if not all Fabric mods should work *currently*
+ return 4; // Quilt would probably be 5
+ return 0;
}
private:
- inline auto getModSearchURL(SearchArgs& args) const -> QString override
+ [[nodiscard]] std::optional<QString> getSearchURL(SearchArgs const& args) const override
{
- auto gameVersionStr = args.versions.size() != 0 ? QString("gameVersion=%1").arg(args.versions.front().toString()) : QString();
+ auto gameVersionStr = args.versions.has_value() ? QString("gameVersion=%1").arg(args.versions.value().front().toString()) : QString();
- return QString(
- "https://api.curseforge.com/v1/mods/search?"
- "gameId=432&"
- "classId=6&"
+ QStringList get_arguments;
+ get_arguments.append(QString("classId=%1").arg(getClassId(args.type)));
+ get_arguments.append(QString("index=%1").arg(args.offset));
+ get_arguments.append("pageSize=25");
+ if (args.search.has_value())
+ get_arguments.append(QString("searchFilter=%1").arg(args.search.value()));
+ if (args.sorting.has_value())
+ get_arguments.append(QString("sortField=%1").arg(args.sorting.value().index));
+ get_arguments.append("sortOrder=desc");
+ if (args.loaders.has_value())
+ get_arguments.append(QString("modLoaderType=%1").arg(getMappedModLoader(args.loaders.value())));
+ get_arguments.append(gameVersionStr);
- "index=%1&"
- "pageSize=25&"
- "searchFilter=%2&"
- "sortField=%3&"
- "sortOrder=desc&"
- "modLoaderType=%4&"
- "%5")
- .arg(args.offset)
- .arg(args.search)
- .arg(getSortFieldInt(args.sorting))
- .arg(getMappedModLoader(args.loaders))
- .arg(gameVersionStr);
+ return "https://api.curseforge.com/v1/mods/search?gameId=432&" + get_arguments.join('&');
};
- inline auto getModInfoURL(QString& id) const -> QString override
+ [[nodiscard]] std::optional<QString> getInfoURL(QString const& id) const override
{
return QString("https://api.curseforge.com/v1/mods/%1").arg(id);
};
- inline auto getVersionsURL(VersionSearchArgs& args) const -> QString override
+ [[nodiscard]] std::optional<QString> getVersionsURL(VersionSearchArgs const& args) const override
{
- QString gameVersionQuery = args.mcVersions.size() == 1 ? QString("gameVersion=%1&").arg(args.mcVersions.front().toString()) : "";
- QString modLoaderQuery = QString("modLoaderType=%1&").arg(getMappedModLoader(args.loaders));
+ QString url{QString("https://api.curseforge.com/v1/mods/%1/files?pageSize=10000&").arg(args.pack.addonId.toString())};
- return QString("https://api.curseforge.com/v1/mods/%1/files?pageSize=10000&%2%3")
- .arg(args.addonId)
- .arg(gameVersionQuery)
- .arg(modLoaderQuery);
- };
+ QStringList get_parameters;
+ if (args.mcVersions.has_value())
+ get_parameters.append(QString("gameVersion=%1").arg(args.mcVersions.value().front().toString()));
+ if (args.loaders.has_value())
+ get_parameters.append(QString("modLoaderType=%1").arg(getMappedModLoader(args.loaders.value())));
- public:
- static auto getMappedModLoader(const ModLoaderTypes loaders) -> int
- {
- // https://docs.curseforge.com/?http#tocS_ModLoaderType
- if (loaders & Forge)
- return 1;
- if (loaders & Fabric)
- return 4;
- // TODO: remove this once Quilt drops official Fabric support
- if (loaders & Quilt) // NOTE: Most if not all Fabric mods should work *currently*
- return 4; // Quilt would probably be 5
- return 0;
- }
+ return url + get_parameters.join('&');
+ };
};
diff --git a/launcher/modplatform/flame/FlameCheckUpdate.cpp b/launcher/modplatform/flame/FlameCheckUpdate.cpp
index 8dd3a846..06a89502 100644
--- a/launcher/modplatform/flame/FlameCheckUpdate.cpp
+++ b/launcher/modplatform/flame/FlameCheckUpdate.cpp
@@ -7,7 +7,10 @@
#include "FileSystem.h"
#include "Json.h"
-#include "ModDownloadTask.h"
+#include "ResourceDownloadTask.h"
+
+#include "minecraft/mod/ModFolderModel.h"
+#include "minecraft/mod/ResourceFolderModel.h"
static FlameAPI api;
@@ -126,7 +129,8 @@ void FlameCheckUpdate::executeTask()
setStatus(tr("Getting API response from CurseForge for '%1'...").arg(mod->name()));
setProgress(i++, m_mods.size());
- auto latest_ver = api.getLatestVersion({ mod->metadata()->project_id.toString(), m_game_versions, m_loaders });
+ ModPlatform::IndexedPack pack{ mod->metadata()->project_id.toString() };
+ auto latest_ver = api.getLatestVersion({ pack, m_game_versions, m_loaders });
// Check if we were aborted while getting the latest version
if (m_was_aborted) {
@@ -160,7 +164,7 @@ void FlameCheckUpdate::executeTask()
for (auto& author : mod->authors())
pack.authors.append({ author });
pack.description = mod->description();
- pack.provider = ModPlatform::Provider::FLAME;
+ pack.provider = ModPlatform::ResourceProvider::FLAME;
auto old_version = mod->version();
if (old_version.isEmpty() && mod->status() != ModStatus::NotInstalled) {
@@ -168,10 +172,10 @@ void FlameCheckUpdate::executeTask()
old_version = current_ver.version;
}
- auto download_task = new ModDownloadTask(pack, latest_ver, m_mods_folder);
+ auto download_task = makeShared<ResourceDownloadTask>(pack, latest_ver, m_mods_folder);
m_updatable.emplace_back(pack.name, mod->metadata()->hash, old_version, latest_ver.version,
api.getModFileChangelog(latest_ver.addonId.toInt(), latest_ver.fileId.toInt()),
- ModPlatform::Provider::FLAME, download_task);
+ ModPlatform::ResourceProvider::FLAME, download_task);
}
}
diff --git a/launcher/modplatform/flame/FlameCheckUpdate.h b/launcher/modplatform/flame/FlameCheckUpdate.h
index 163c706c..4a98d684 100644
--- a/launcher/modplatform/flame/FlameCheckUpdate.h
+++ b/launcher/modplatform/flame/FlameCheckUpdate.h
@@ -8,7 +8,7 @@ class FlameCheckUpdate : public CheckUpdateTask {
Q_OBJECT
public:
- FlameCheckUpdate(QList<Mod*>& mods, std::list<Version>& mcVersions, ModAPI::ModLoaderTypes loaders, std::shared_ptr<ModFolderModel> mods_folder)
+ FlameCheckUpdate(QList<Mod*>& mods, std::list<Version>& mcVersions, std::optional<ResourceAPI::ModLoaderTypes> loaders, std::shared_ptr<ModFolderModel> mods_folder)
: CheckUpdateTask(mods, mcVersions, loaders, mods_folder)
{}
diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp
index dc69769a..964b559c 100644
--- a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp
+++ b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp
@@ -183,7 +183,7 @@ bool FlameCreationTask::updateInstance()
QEventLoop loop;
- connect(job, &NetJob::succeeded, this, [this, raw_response, fileIds, old_inst_dir, &old_files, old_minecraft_dir] {
+ connect(job.get(), &Task::succeeded, this, [this, raw_response, fileIds, old_inst_dir, &old_files, old_minecraft_dir] {
// Parse the API response
QJsonParseError parse_error{};
auto doc = QJsonDocument::fromJson(*raw_response, &parse_error);
@@ -225,7 +225,7 @@ bool FlameCreationTask::updateInstance()
m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(relative_path));
}
});
- connect(job, &NetJob::finished, &loop, &QEventLoop::quit);
+ connect(job.get(), &Task::finished, &loop, &QEventLoop::quit);
m_process_update_file_info_job = job;
job->start();
@@ -373,7 +373,7 @@ bool FlameCreationTask::createInstance()
instance.setManagedPack("flame", m_managed_id, m_pack.name, m_managed_version_id, m_pack.version);
instance.setName(name());
- m_mod_id_resolver = new Flame::FileResolvingTask(APPLICATION->network(), m_pack);
+ m_mod_id_resolver.reset(new Flame::FileResolvingTask(APPLICATION->network(), m_pack));
connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::succeeded, this, [this, &loop] { idResolverSucceeded(loop); });
connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::failed, [&](QString reason) {
m_mod_id_resolver.reset();
@@ -452,7 +452,7 @@ void FlameCreationTask::idResolverSucceeded(QEventLoop& loop)
void FlameCreationTask::setupDownloadJob(QEventLoop& loop)
{
- m_files_job = new NetJob(tr("Mod download"), APPLICATION->network());
+ m_files_job.reset(new NetJob(tr("Mod download"), APPLICATION->network()));
for (const auto& result : m_mod_id_resolver->getResults().files) {
QString filename = result.fileName;
if (!result.required) {
diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.h b/launcher/modplatform/flame/FlameInstanceCreationTask.h
index 498e1d6e..0ae4735b 100644
--- a/launcher/modplatform/flame/FlameInstanceCreationTask.h
+++ b/launcher/modplatform/flame/FlameInstanceCreationTask.h
@@ -86,7 +86,7 @@ class FlameCreationTask final : public InstanceCreationTask {
Flame::Manifest m_pack;
// Handle to allow aborting
- NetJob* m_process_update_file_info_job = nullptr;
+ Task::Ptr m_process_update_file_info_job = nullptr;
NetJob::Ptr m_files_job = nullptr;
QString m_managed_id, m_managed_version_id;
diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp
index 32aa4bdb..7498e830 100644
--- a/launcher/modplatform/flame/FlameModIndex.cpp
+++ b/launcher/modplatform/flame/FlameModIndex.cpp
@@ -11,7 +11,7 @@ static ModPlatform::ProviderCapabilities ProviderCaps;
void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj)
{
pack.addonId = Json::requireInteger(obj, "id");
- pack.provider = ModPlatform::Provider::FLAME;
+ pack.provider = ModPlatform::ResourceProvider::FLAME;
pack.name = Json::requireString(obj, "name");
pack.slug = Json::requireString(obj, "slug");
pack.websiteUrl = Json::ensureString(Json::ensureObject(obj, "links"), "websiteUrl", "");
@@ -76,10 +76,10 @@ static QString enumToString(int hash_algorithm)
void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
QJsonArray& arr,
const shared_qobject_ptr<QNetworkAccessManager>& network,
- BaseInstance* inst)
+ const BaseInstance* inst)
{
QVector<ModPlatform::IndexedVersion> unsortedVersions;
- auto profile = (dynamic_cast<MinecraftInstance*>(inst))->getPackProfile();
+ auto profile = (dynamic_cast<const MinecraftInstance*>(inst))->getPackProfile();
QString mcVersion = profile->getComponentVersion("net.minecraft");
for (auto versionIter : arr) {
@@ -127,7 +127,7 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) ->
auto hash_list = Json::ensureArray(obj, "hashes");
for (auto h : hash_list) {
auto hash_entry = Json::ensureObject(h);
- auto hash_types = ProviderCaps.hashType(ModPlatform::Provider::FLAME);
+ auto hash_types = ProviderCaps.hashType(ModPlatform::ResourceProvider::FLAME);
auto hash_algo = enumToString(Json::ensureInteger(hash_entry, "algo", 1, "algorithm"));
if (hash_types.contains(hash_algo)) {
file.hash = Json::requireString(hash_entry, "value");
diff --git a/launcher/modplatform/flame/FlameModIndex.h b/launcher/modplatform/flame/FlameModIndex.h
index db63cdbb..33c4a529 100644
--- a/launcher/modplatform/flame/FlameModIndex.h
+++ b/launcher/modplatform/flame/FlameModIndex.h
@@ -17,7 +17,7 @@ void loadBody(ModPlatform::IndexedPack& m, QJsonObject& obj);
void loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
QJsonArray& arr,
const shared_qobject_ptr<QNetworkAccessManager>& network,
- BaseInstance* inst);
+ const BaseInstance* inst);
auto loadIndexedPackVersion(QJsonObject& obj, bool load_changelog = false) -> ModPlatform::IndexedVersion;
} // namespace FlameMod
diff --git a/launcher/modplatform/helpers/HashUtils.cpp b/launcher/modplatform/helpers/HashUtils.cpp
index f1e4759e..2177ddad 100644
--- a/launcher/modplatform/helpers/HashUtils.cpp
+++ b/launcher/modplatform/helpers/HashUtils.cpp
@@ -12,12 +12,12 @@ namespace Hashing {
static ModPlatform::ProviderCapabilities ProviderCaps;
-Hasher::Ptr createHasher(QString file_path, ModPlatform::Provider provider)
+Hasher::Ptr createHasher(QString file_path, ModPlatform::ResourceProvider provider)
{
switch (provider) {
- case ModPlatform::Provider::MODRINTH:
+ case ModPlatform::ResourceProvider::MODRINTH:
return createModrinthHasher(file_path);
- case ModPlatform::Provider::FLAME:
+ case ModPlatform::ResourceProvider::FLAME:
return createFlameHasher(file_path);
default:
qCritical() << "[Hashing]"
@@ -28,22 +28,22 @@ Hasher::Ptr createHasher(QString file_path, ModPlatform::Provider provider)
Hasher::Ptr createModrinthHasher(QString file_path)
{
- return new ModrinthHasher(file_path);
+ return makeShared<ModrinthHasher>(file_path);
}
Hasher::Ptr createFlameHasher(QString file_path)
{
- return new FlameHasher(file_path);
+ return makeShared<FlameHasher>(file_path);
}
-Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::Provider provider)
+Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::ResourceProvider provider)
{
- return new BlockedModHasher(file_path, provider);
+ return makeShared<BlockedModHasher>(file_path, provider);
}
-Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::Provider provider, QString type)
+Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::ResourceProvider provider, QString type)
{
- auto hasher = new BlockedModHasher(file_path, provider);
+ auto hasher = makeShared<BlockedModHasher>(file_path, provider);
hasher->useHashType(type);
return hasher;
}
@@ -62,8 +62,8 @@ void ModrinthHasher::executeTask()
return;
}
- auto hash_type = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH).first();
- m_hash = ProviderCaps.hash(ModPlatform::Provider::MODRINTH, &file, hash_type);
+ auto hash_type = ProviderCaps.hashType(ModPlatform::ResourceProvider::MODRINTH).first();
+ m_hash = ProviderCaps.hash(ModPlatform::ResourceProvider::MODRINTH, &file, hash_type);
file.close();
@@ -92,7 +92,7 @@ void FlameHasher::executeTask()
}
-BlockedModHasher::BlockedModHasher(QString file_path, ModPlatform::Provider provider)
+BlockedModHasher::BlockedModHasher(QString file_path, ModPlatform::ResourceProvider provider)
: Hasher(file_path), provider(provider) {
setObjectName(QString("BlockedModHasher: %1").arg(file_path));
hash_type = ProviderCaps.hashType(provider).first();
diff --git a/launcher/modplatform/helpers/HashUtils.h b/launcher/modplatform/helpers/HashUtils.h
index fa3244f6..91146a52 100644
--- a/launcher/modplatform/helpers/HashUtils.h
+++ b/launcher/modplatform/helpers/HashUtils.h
@@ -42,21 +42,21 @@ class ModrinthHasher : public Hasher {
class BlockedModHasher : public Hasher {
public:
- BlockedModHasher(QString file_path, ModPlatform::Provider provider);
+ BlockedModHasher(QString file_path, ModPlatform::ResourceProvider provider);
void executeTask() override;
QStringList getHashTypes();
bool useHashType(QString type);
private:
- ModPlatform::Provider provider;
+ ModPlatform::ResourceProvider provider;
QString hash_type;
};
-Hasher::Ptr createHasher(QString file_path, ModPlatform::Provider provider);
+Hasher::Ptr createHasher(QString file_path, ModPlatform::ResourceProvider 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);
+Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::ResourceProvider provider);
+Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::ResourceProvider provider, QString type);
} // namespace Hashing
diff --git a/launcher/modplatform/helpers/NetworkModAPI.cpp b/launcher/modplatform/helpers/NetworkModAPI.cpp
deleted file mode 100644
index 7633030e..00000000
--- a/launcher/modplatform/helpers/NetworkModAPI.cpp
+++ /dev/null
@@ -1,97 +0,0 @@
-#include "NetworkModAPI.h"
-
-#include "ui/pages/modplatform/ModModel.h"
-
-#include "Application.h"
-#include "net/NetJob.h"
-
-void NetworkModAPI::searchMods(CallerType* caller, SearchArgs&& args) const
-{
- auto netJob = new NetJob(QString("%1::Search").arg(caller->debugName()), APPLICATION->network());
- auto searchUrl = getModSearchURL(args);
-
- auto response = new QByteArray();
- netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), response));
-
- QObject::connect(netJob, &NetJob::started, caller, [caller, netJob] { caller->setActiveJob(netJob); });
- QObject::connect(netJob, &NetJob::failed, caller, &CallerType::searchRequestFailed);
- QObject::connect(netJob, &NetJob::aborted, caller, &CallerType::searchRequestAborted);
- QObject::connect(netJob, &NetJob::succeeded, caller, [caller, response] {
- QJsonParseError parse_error{};
- QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
- if (parse_error.error != QJsonParseError::NoError) {
- qWarning() << "Error while parsing JSON response from " << caller->debugName() << " at " << parse_error.offset
- << " reason: " << parse_error.errorString();
- qWarning() << *response;
- return;
- }
-
- caller->searchRequestFinished(doc);
- });
-
- netJob->start();
-}
-
-void NetworkModAPI::getModInfo(ModPlatform::IndexedPack& pack, std::function<void(QJsonDocument&, ModPlatform::IndexedPack&)> callback)
-{
- auto response = new QByteArray();
- auto job = getProject(pack.addonId.toString(), response);
-
- QObject::connect(job, &NetJob::succeeded, [callback, &pack, response] {
- QJsonParseError parse_error{};
- QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
- if (parse_error.error != QJsonParseError::NoError) {
- qWarning() << "Error while parsing JSON response for mod info at " << parse_error.offset
- << " reason: " << parse_error.errorString();
- qWarning() << *response;
- return;
- }
-
- callback(doc, pack);
- });
-
- job->start();
-}
-
-void NetworkModAPI::getVersions(VersionSearchArgs&& args, std::function<void(QJsonDocument&, QString)> callback) const
-{
- auto netJob = new NetJob(QString("ModVersions(%2)").arg(args.addonId), APPLICATION->network());
- auto response = new QByteArray();
-
- netJob->addNetAction(Net::Download::makeByteArray(getVersionsURL(args), response));
-
- QObject::connect(netJob, &NetJob::succeeded, [response, callback, args] {
- QJsonParseError parse_error{};
- QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
- if (parse_error.error != QJsonParseError::NoError) {
- qWarning() << "Error while parsing JSON response for getting versions at " << parse_error.offset
- << " reason: " << parse_error.errorString();
- qWarning() << *response;
- return;
- }
-
- callback(doc, args.addonId);
- });
-
- QObject::connect(netJob, &NetJob::finished, [response, netJob] {
- netJob->deleteLater();
- delete response;
- });
-
- netJob->start();
-}
-
-auto NetworkModAPI::getProject(QString addonId, QByteArray* response) const -> NetJob*
-{
- auto netJob = new NetJob(QString("%1::GetProject").arg(addonId), APPLICATION->network());
- auto searchUrl = getModInfoURL(addonId);
-
- netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), response));
-
- QObject::connect(netJob, &NetJob::finished, [response, netJob] {
- netJob->deleteLater();
- delete response;
- });
-
- return netJob;
-}
diff --git a/launcher/modplatform/helpers/NetworkModAPI.h b/launcher/modplatform/helpers/NetworkModAPI.h
deleted file mode 100644
index b8af22c7..00000000
--- a/launcher/modplatform/helpers/NetworkModAPI.h
+++ /dev/null
@@ -1,17 +0,0 @@
-#pragma once
-
-#include "modplatform/ModAPI.h"
-
-class NetworkModAPI : public ModAPI {
- public:
- void searchMods(CallerType* caller, SearchArgs&& args) const override;
- void getModInfo(ModPlatform::IndexedPack& pack, std::function<void(QJsonDocument&, ModPlatform::IndexedPack&)> callback) override;
- void getVersions(VersionSearchArgs&& args, std::function<void(QJsonDocument&, QString)> callback) const override;
-
- auto getProject(QString addonId, QByteArray* response) const -> NetJob* override;
-
- protected:
- virtual auto getModSearchURL(SearchArgs& args) const -> QString = 0;
- virtual auto getModInfoURL(QString& id) const -> QString = 0;
- virtual auto getVersionsURL(VersionSearchArgs& args) const -> QString = 0;
-};
diff --git a/launcher/modplatform/helpers/NetworkResourceAPI.cpp b/launcher/modplatform/helpers/NetworkResourceAPI.cpp
new file mode 100644
index 00000000..010ac15e
--- /dev/null
+++ b/launcher/modplatform/helpers/NetworkResourceAPI.cpp
@@ -0,0 +1,128 @@
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
+#include "NetworkResourceAPI.h"
+
+#include "Application.h"
+#include "net/NetJob.h"
+
+#include "modplatform/ModIndex.h"
+
+Task::Ptr NetworkResourceAPI::searchProjects(SearchArgs&& args, SearchCallbacks&& callbacks) const
+{
+ auto search_url_optional = getSearchURL(args);
+ if (!search_url_optional.has_value()) {
+ callbacks.on_fail("Failed to create search URL", -1);
+ return nullptr;
+ }
+
+ auto search_url = search_url_optional.value();
+
+ auto response = new QByteArray();
+ auto netJob = makeShared<NetJob>(QString("%1::Search").arg(debugName()), APPLICATION->network());
+
+ netJob->addNetAction(Net::Download::makeByteArray(QUrl(search_url), response));
+
+ QObject::connect(netJob.get(), &NetJob::succeeded, [=]{
+ QJsonParseError parse_error{};
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
+ if (parse_error.error != QJsonParseError::NoError) {
+ qWarning() << "Error while parsing JSON response from " << debugName() << " at " << parse_error.offset
+ << " reason: " << parse_error.errorString();
+ qWarning() << *response;
+
+ callbacks.on_fail(parse_error.errorString(), -1);
+
+ return;
+ }
+
+ callbacks.on_succeed(doc);
+ });
+
+ QObject::connect(netJob.get(), &NetJob::failed, [=](QString reason){
+ int network_error_code = -1;
+ if (auto* failed_action = netJob->getFailedActions().at(0); failed_action && failed_action->m_reply)
+ network_error_code = failed_action->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ callbacks.on_fail(reason, network_error_code);
+ });
+ QObject::connect(netJob.get(), &NetJob::aborted, [=]{
+ callbacks.on_abort();
+ });
+
+ return netJob;
+}
+
+Task::Ptr NetworkResourceAPI::getProjectInfo(ProjectInfoArgs&& args, ProjectInfoCallbacks&& callbacks) const
+{
+ auto response = new QByteArray();
+ auto job = getProject(args.pack.addonId.toString(), response);
+
+ QObject::connect(job.get(), &NetJob::succeeded, [response, callbacks, args] {
+ QJsonParseError parse_error{};
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
+ if (parse_error.error != QJsonParseError::NoError) {
+ qWarning() << "Error while parsing JSON response for mod info at " << parse_error.offset
+ << " reason: " << parse_error.errorString();
+ qWarning() << *response;
+ return;
+ }
+
+ callbacks.on_succeed(doc, args.pack);
+ });
+
+ return job;
+}
+
+Task::Ptr NetworkResourceAPI::getProjectVersions(VersionSearchArgs&& args, VersionSearchCallbacks&& callbacks) const
+{
+ auto versions_url_optional = getVersionsURL(args);
+ if (!versions_url_optional.has_value())
+ return nullptr;
+
+ auto versions_url = versions_url_optional.value();
+
+ auto netJob = makeShared<NetJob>(QString("%1::Versions").arg(args.pack.name), APPLICATION->network());
+ auto response = new QByteArray();
+
+ netJob->addNetAction(Net::Download::makeByteArray(versions_url, response));
+
+ QObject::connect(netJob.get(), &NetJob::succeeded, [=] {
+ QJsonParseError parse_error{};
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
+ if (parse_error.error != QJsonParseError::NoError) {
+ qWarning() << "Error while parsing JSON response for getting versions at " << parse_error.offset
+ << " reason: " << parse_error.errorString();
+ qWarning() << *response;
+ return;
+ }
+
+ callbacks.on_succeed(doc, args.pack);
+ });
+
+ QObject::connect(netJob.get(), &NetJob::finished, [response] {
+ delete response;
+ });
+
+ return netJob;
+}
+
+Task::Ptr NetworkResourceAPI::getProject(QString addonId, QByteArray* response) const
+{
+ auto project_url_optional = getInfoURL(addonId);
+ if (!project_url_optional.has_value())
+ return nullptr;
+
+ auto project_url = project_url_optional.value();
+
+ auto netJob = makeShared<NetJob>(QString("%1::GetProject").arg(addonId), APPLICATION->network());
+
+ netJob->addNetAction(Net::Download::makeByteArray(QUrl(project_url), response));
+
+ QObject::connect(netJob.get(), &NetJob::finished, [response] {
+ delete response;
+ });
+
+ return netJob;
+}
diff --git a/launcher/modplatform/helpers/NetworkResourceAPI.h b/launcher/modplatform/helpers/NetworkResourceAPI.h
new file mode 100644
index 00000000..94813bec
--- /dev/null
+++ b/launcher/modplatform/helpers/NetworkResourceAPI.h
@@ -0,0 +1,22 @@
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
+#pragma once
+
+#include "modplatform/ResourceAPI.h"
+
+class NetworkResourceAPI : public ResourceAPI {
+ public:
+ Task::Ptr searchProjects(SearchArgs&&, SearchCallbacks&&) const override;
+
+ Task::Ptr getProject(QString addonId, QByteArray* response) const override;
+
+ Task::Ptr getProjectInfo(ProjectInfoArgs&&, ProjectInfoCallbacks&&) const override;
+ Task::Ptr getProjectVersions(VersionSearchArgs&&, VersionSearchCallbacks&&) const override;
+
+ protected:
+ [[nodiscard]] virtual auto getSearchURL(SearchArgs const& args) const -> std::optional<QString> = 0;
+ [[nodiscard]] virtual auto getInfoURL(QString const& id) const -> std::optional<QString> = 0;
+ [[nodiscard]] virtual auto getVersionsURL(VersionSearchArgs const& args) const -> std::optional<QString> = 0;
+};
diff --git a/launcher/modplatform/legacy_ftb/PackFetchTask.cpp b/launcher/modplatform/legacy_ftb/PackFetchTask.cpp
index 36aa60c7..e8768c5c 100644
--- a/launcher/modplatform/legacy_ftb/PackFetchTask.cpp
+++ b/launcher/modplatform/legacy_ftb/PackFetchTask.cpp
@@ -47,7 +47,7 @@ void PackFetchTask::fetch()
publicPacks.clear();
thirdPartyPacks.clear();
- jobPtr = new NetJob("LegacyFTB::ModpackFetch", m_network);
+ jobPtr.reset(new NetJob("LegacyFTB::ModpackFetch", m_network));
QUrl publicPacksUrl = QUrl(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/modpacks.xml");
qDebug() << "Downloading public version info from" << publicPacksUrl.toString();
diff --git a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp
index 06b3788b..8d45fc5c 100644
--- a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp
+++ b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp
@@ -69,7 +69,7 @@ void PackInstallTask::downloadPack()
archivePath = QString("%1/%2/%3").arg(m_pack.dir, m_version.replace(".", "_"), m_pack.file);
- netJobContainer = new NetJob("Download FTB Pack", m_network);
+ netJobContainer.reset(new NetJob("Download FTB Pack", m_network));
QString url;
if (m_pack.type == PackType::Private) {
url = QString(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "privatepacks/%1").arg(archivePath);
diff --git a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp
index 2979663d..68d4751c 100644
--- a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp
+++ b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp
@@ -87,15 +87,15 @@ void PackInstallTask::executeTask()
auto version = *version_it;
- auto* netJob = new NetJob("ModpacksCH::VersionFetch", APPLICATION->network());
+ auto netJob = makeShared<NetJob>("ModpacksCH::VersionFetch", APPLICATION->network());
auto searchUrl = QString(BuildConfig.MODPACKSCH_API_BASE_URL + "public/modpack/%1/%2").arg(m_pack.id).arg(version.id);
netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &m_response));
- QObject::connect(netJob, &NetJob::succeeded, this, &PackInstallTask::onManifestDownloadSucceeded);
- QObject::connect(netJob, &NetJob::failed, this, &PackInstallTask::onManifestDownloadFailed);
- QObject::connect(netJob, &NetJob::aborted, this, &PackInstallTask::abort);
- QObject::connect(netJob, &NetJob::progress, this, &PackInstallTask::setProgress);
+ QObject::connect(netJob.get(), &NetJob::succeeded, this, &PackInstallTask::onManifestDownloadSucceeded);
+ QObject::connect(netJob.get(), &NetJob::failed, this, &PackInstallTask::onManifestDownloadFailed);
+ QObject::connect(netJob.get(), &NetJob::aborted, this, &PackInstallTask::abort);
+ QObject::connect(netJob.get(), &NetJob::progress, this, &PackInstallTask::setProgress);
m_net_job = netJob;
@@ -162,7 +162,7 @@ void PackInstallTask::resolveMods()
index++;
}
- m_mod_id_resolver_task = new Flame::FileResolvingTask(APPLICATION->network(), manifest);
+ m_mod_id_resolver_task.reset(new Flame::FileResolvingTask(APPLICATION->network(), manifest));
connect(m_mod_id_resolver_task.get(), &Flame::FileResolvingTask::succeeded, this, &PackInstallTask::onResolveModsSucceeded);
connect(m_mod_id_resolver_task.get(), &Flame::FileResolvingTask::failed, this, &PackInstallTask::onResolveModsFailed);
@@ -294,7 +294,7 @@ void PackInstallTask::downloadPack()
setStatus(tr("Downloading mods..."));
setAbortable(false);
- auto* jobPtr = new NetJob(tr("Mod download"), APPLICATION->network());
+ auto jobPtr = makeShared<NetJob>(tr("Mod download"), APPLICATION->network());
for (auto const& file : m_version.files) {
if (file.serverOnly || file.url.isEmpty())
continue;
@@ -313,10 +313,10 @@ void PackInstallTask::downloadPack()
jobPtr->addNetAction(dl);
}
- connect(jobPtr, &NetJob::succeeded, this, &PackInstallTask::onModDownloadSucceeded);
- connect(jobPtr, &NetJob::failed, this, &PackInstallTask::onModDownloadFailed);
- connect(jobPtr, &NetJob::aborted, this, &PackInstallTask::abort);
- connect(jobPtr, &NetJob::progress, this, &PackInstallTask::setProgress);
+ connect(jobPtr.get(), &NetJob::succeeded, this, &PackInstallTask::onModDownloadSucceeded);
+ connect(jobPtr.get(), &NetJob::failed, this, &PackInstallTask::onModDownloadFailed);
+ connect(jobPtr.get(), &NetJob::aborted, this, &PackInstallTask::abort);
+ connect(jobPtr.get(), &NetJob::progress, this, &PackInstallTask::setProgress);
m_net_job = jobPtr;
diff --git a/launcher/modplatform/modrinth/ModrinthAPI.cpp b/launcher/modplatform/modrinth/ModrinthAPI.cpp
index 747cf4c3..29e3d129 100644
--- a/launcher/modplatform/modrinth/ModrinthAPI.cpp
+++ b/launcher/modplatform/modrinth/ModrinthAPI.cpp
@@ -1,24 +1,29 @@
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
#include "ModrinthAPI.h"
#include "Application.h"
#include "Json.h"
+#include "net/NetJob.h"
#include "net/Upload.h"
-auto ModrinthAPI::currentVersion(QString hash, QString hash_format, QByteArray* response) -> NetJob::Ptr
+Task::Ptr ModrinthAPI::currentVersion(QString hash, QString hash_format, QByteArray* response)
{
- auto* netJob = new NetJob(QString("Modrinth::GetCurrentVersion"), APPLICATION->network());
+ auto netJob = makeShared<NetJob>(QString("Modrinth::GetCurrentVersion"), APPLICATION->network());
netJob->addNetAction(Net::Download::makeByteArray(
QString(BuildConfig.MODRINTH_PROD_URL + "/version_file/%1?algorithm=%2").arg(hash, hash_format), response));
- QObject::connect(netJob, &NetJob::finished, [response] { delete response; });
+ QObject::connect(netJob.get(), &NetJob::finished, [response] { delete response; });
return netJob;
}
-auto ModrinthAPI::currentVersions(const QStringList& hashes, QString hash_format, QByteArray* response) -> NetJob::Ptr
+Task::Ptr ModrinthAPI::currentVersions(const QStringList& hashes, QString hash_format, QByteArray* response)
{
- auto* netJob = new NetJob(QString("Modrinth::GetCurrentVersions"), APPLICATION->network());
+ auto netJob = makeShared<NetJob>(QString("Modrinth::GetCurrentVersions"), APPLICATION->network());
QJsonObject body_obj;
@@ -30,28 +35,31 @@ auto ModrinthAPI::currentVersions(const QStringList& hashes, QString hash_format
netJob->addNetAction(Net::Upload::makeByteArray(QString(BuildConfig.MODRINTH_PROD_URL + "/version_files"), response, body_raw));
- QObject::connect(netJob, &NetJob::finished, [response] { delete response; });
+ QObject::connect(netJob.get(), &NetJob::finished, [response] { delete response; });
return netJob;
}
-auto ModrinthAPI::latestVersion(QString hash,
- QString hash_format,
- std::list<Version> mcVersions,
- ModLoaderTypes loaders,
- QByteArray* response) -> NetJob::Ptr
+Task::Ptr ModrinthAPI::latestVersion(QString hash,
+ QString hash_format,
+ std::optional<std::list<Version>> mcVersions,
+ std::optional<ModLoaderTypes> loaders,
+ QByteArray* response)
{
- auto* netJob = new NetJob(QString("Modrinth::GetLatestVersion"), APPLICATION->network());
+ auto netJob = makeShared<NetJob>(QString("Modrinth::GetLatestVersion"), APPLICATION->network());
QJsonObject body_obj;
- Json::writeStringList(body_obj, "loaders", getModLoaderStrings(loaders));
+ if (loaders.has_value())
+ Json::writeStringList(body_obj, "loaders", getModLoaderStrings(loaders.value()));
- QStringList game_versions;
- for (auto& ver : mcVersions) {
- game_versions.append(ver.toString());
+ if (mcVersions.has_value()) {
+ QStringList game_versions;
+ for (auto& ver : mcVersions.value()) {
+ game_versions.append(ver.toString());
+ }
+ Json::writeStringList(body_obj, "game_versions", game_versions);
}
- Json::writeStringList(body_obj, "game_versions", game_versions);
QJsonDocument body(body_obj);
auto body_raw = body.toJson();
@@ -59,50 +67,67 @@ auto ModrinthAPI::latestVersion(QString hash,
netJob->addNetAction(Net::Upload::makeByteArray(
QString(BuildConfig.MODRINTH_PROD_URL + "/version_file/%1/update?algorithm=%2").arg(hash, hash_format), response, body_raw));
- QObject::connect(netJob, &NetJob::finished, [response] { delete response; });
+ QObject::connect(netJob.get(), &NetJob::finished, [response] { delete response; });
return netJob;
}
-auto ModrinthAPI::latestVersions(const QStringList& hashes,
- QString hash_format,
- std::list<Version> mcVersions,
- ModLoaderTypes loaders,
- QByteArray* response) -> NetJob::Ptr
+Task::Ptr ModrinthAPI::latestVersions(const QStringList& hashes,
+ QString hash_format,
+ std::optional<std::list<Version>> mcVersions,
+ std::optional<ModLoaderTypes> loaders,
+ QByteArray* response)
{
- auto* netJob = new NetJob(QString("Modrinth::GetLatestVersions"), APPLICATION->network());
+ auto netJob = makeShared<NetJob>(QString("Modrinth::GetLatestVersions"), APPLICATION->network());
QJsonObject body_obj;
Json::writeStringList(body_obj, "hashes", hashes);
Json::writeString(body_obj, "algorithm", hash_format);
- Json::writeStringList(body_obj, "loaders", getModLoaderStrings(loaders));
+ if (loaders.has_value())
+ Json::writeStringList(body_obj, "loaders", getModLoaderStrings(loaders.value()));
- QStringList game_versions;
- for (auto& ver : mcVersions) {
- game_versions.append(ver.toString());
+ if (mcVersions.has_value()) {
+ QStringList game_versions;
+ for (auto& ver : mcVersions.value()) {
+ game_versions.append(ver.toString());
+ }
+ Json::writeStringList(body_obj, "game_versions", game_versions);
}
- Json::writeStringList(body_obj, "game_versions", game_versions);
QJsonDocument body(body_obj);
auto body_raw = body.toJson();
netJob->addNetAction(Net::Upload::makeByteArray(QString(BuildConfig.MODRINTH_PROD_URL + "/version_files/update"), response, body_raw));
- QObject::connect(netJob, &NetJob::finished, [response] { delete response; });
+ QObject::connect(netJob.get(), &NetJob::finished, [response] { delete response; });
return netJob;
}
-auto ModrinthAPI::getProjects(QStringList addonIds, QByteArray* response) const -> NetJob*
+Task::Ptr ModrinthAPI::getProjects(QStringList addonIds, QByteArray* response) const
{
- auto netJob = new NetJob(QString("Modrinth::GetProjects"), APPLICATION->network());
+ auto netJob = makeShared<NetJob>(QString("Modrinth::GetProjects"), APPLICATION->network());
auto searchUrl = getMultipleModInfoURL(addonIds);
netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), response));
- QObject::connect(netJob, &NetJob::finished, [response, netJob] { delete response; netJob->deleteLater(); });
+ QObject::connect(netJob.get(), &NetJob::finished, [response, netJob] {
+ delete response;
+ });
return netJob;
}
+
+// https://docs.modrinth.com/api-spec/#tag/projects/operation/searchProjects
+static QList<ResourceAPI::SortingMethod> s_sorts = { { 1, "relevance", QObject::tr("Sort by Relevance") },
+ { 2, "downloads", QObject::tr("Sort by Downloads") },
+ { 3, "follows", QObject::tr("Sort by Follows") },
+ { 4, "newest", QObject::tr("Sort by Last Updated") },
+ { 5, "updated", QObject::tr("Sort by Newest") } };
+
+QList<ResourceAPI::SortingMethod> ModrinthAPI::getSortingMethods() const
+{
+ return s_sorts;
+}
diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h
index e1a18681..dda27303 100644
--- a/launcher/modplatform/modrinth/ModrinthAPI.h
+++ b/launcher/modplatform/modrinth/ModrinthAPI.h
@@ -1,69 +1,54 @@
+// SPDX-FileCopyrightText: 2022-2023 flowln <flowlnlnln@gmail.com>
+//
// SPDX-License-Identifier: GPL-3.0-only
-/*
- * PolyMC - Minecraft Launcher
- * Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
#pragma once
#include "BuildConfig.h"
-#include "modplatform/ModAPI.h"
#include "modplatform/ModIndex.h"
-#include "modplatform/helpers/NetworkModAPI.h"
+#include "modplatform/helpers/NetworkResourceAPI.h"
#include <QDebug>
-class ModrinthAPI : public NetworkModAPI {
+class ModrinthAPI : public NetworkResourceAPI {
public:
auto currentVersion(QString hash,
QString hash_format,
- QByteArray* response) -> NetJob::Ptr;
+ QByteArray* response) -> Task::Ptr;
auto currentVersions(const QStringList& hashes,
QString hash_format,
- QByteArray* response) -> NetJob::Ptr;
+ QByteArray* response) -> Task::Ptr;
auto latestVersion(QString hash,
QString hash_format,
- std::list<Version> mcVersions,
- ModLoaderTypes loaders,
- QByteArray* response) -> NetJob::Ptr;
+ std::optional<std::list<Version>> mcVersions,
+ std::optional<ModLoaderTypes> loaders,
+ QByteArray* response) -> Task::Ptr;
auto latestVersions(const QStringList& hashes,
QString hash_format,
- std::list<Version> mcVersions,
- ModLoaderTypes loaders,
- QByteArray* response) -> NetJob::Ptr;
+ std::optional<std::list<Version>> mcVersions,
+ std::optional<ModLoaderTypes> loaders,
+ QByteArray* response) -> Task::Ptr;
- auto getProjects(QStringList addonIds, QByteArray* response) const -> NetJob* override;
+ Task::Ptr getProjects(QStringList addonIds, QByteArray* response) const override;
public:
+ [[nodiscard]] auto getSortingMethods() const -> QList<ResourceAPI::SortingMethod> override;
+
inline auto getAuthorURL(const QString& name) const -> QString { return "https://modrinth.com/user/" + name; };
static auto getModLoaderStrings(const ModLoaderTypes types) -> const QStringList
{
QStringList l;
- for (auto loader : {Forge, Fabric, Quilt})
- {
- if ((types & loader) || types == Unspecified)
- {
- l << ModAPI::getModLoaderString(loader);
+ for (auto loader : {Forge, Fabric, Quilt}) {
+ if (types & loader) {
+ l << getModLoaderString(loader);
}
}
if ((types & Quilt) && (~types & Fabric)) // Add Fabric if Quilt is in use, if Fabric isn't already there
- l << ModAPI::getModLoaderString(Fabric);
+ l << getModLoaderString(Fabric);
return l;
}
@@ -78,28 +63,54 @@ class ModrinthAPI : public NetworkModAPI {
}
private:
- inline auto getModSearchURL(SearchArgs& args) const -> QString override
+ [[nodiscard]] static QString resourceTypeParameter(ModPlatform::ResourceType type)
+ {
+ switch (type) {
+ case ModPlatform::ResourceType::MOD:
+ return "mod";
+ default:
+ qWarning() << "Invalid resource type for Modrinth API!";
+ break;
+ }
+
+ return "";
+ }
+ [[nodiscard]] QString createFacets(SearchArgs const& args) const
+ {
+ QStringList facets_list;
+
+ if (args.loaders.has_value())
+ facets_list.append(QString("[%1]").arg(getModLoaderFilters(args.loaders.value())));
+ if (args.versions.has_value())
+ facets_list.append(QString("[%1]").arg(getGameVersionsArray(args.versions.value())));
+ facets_list.append(QString("[\"project_type:%1\"]").arg(resourceTypeParameter(args.type)));
+
+ return QString("[%1]").arg(facets_list.join(','));
+ }
+
+ public:
+ [[nodiscard]] inline auto getSearchURL(SearchArgs const& args) const -> std::optional<QString> override
{
- if (!validateModLoaders(args.loaders)) {
- qWarning() << "Modrinth only have Forge and Fabric-compatible mods!";
- return "";
+ if (args.loaders.has_value()) {
+ if (!validateModLoaders(args.loaders.value())) {
+ qWarning() << "Modrinth only have Forge and Fabric-compatible mods!";
+ return {};
+ }
}
- return QString(BuildConfig.MODRINTH_PROD_URL +
- "/search?"
- "offset=%1&"
- "limit=25&"
- "query=%2&"
- "index=%3&"
- "facets=[[%4],%5[\"project_type:mod\"]]")
- .arg(args.offset)
- .arg(args.search)
- .arg(args.sorting)
- .arg(getModLoaderFilters(args.loaders))
- .arg(getGameVersionsArray(args.versions));
+ QStringList get_arguments;
+ get_arguments.append(QString("offset=%1").arg(args.offset));
+ get_arguments.append(QString("limit=25"));
+ if (args.search.has_value())
+ get_arguments.append(QString("query=%1").arg(args.search.value()));
+ if (args.sorting.has_value())
+ get_arguments.append(QString("index=%1").arg(args.sorting.value().name));
+ get_arguments.append(QString("facets=%1").arg(createFacets(args)));
+
+ return BuildConfig.MODRINTH_PROD_URL + "/search?" + get_arguments.join('&');
};
- inline auto getModInfoURL(QString& id) const -> QString override
+ inline auto getInfoURL(QString const& id) const -> std::optional<QString> override
{
return BuildConfig.MODRINTH_PROD_URL + "/project/" + id;
};
@@ -109,15 +120,16 @@ class ModrinthAPI : public NetworkModAPI {
return BuildConfig.MODRINTH_PROD_URL + QString("/projects?ids=[\"%1\"]").arg(ids.join("\",\""));
};
- inline auto getVersionsURL(VersionSearchArgs& args) const -> QString override
+ inline auto getVersionsURL(VersionSearchArgs const& args) const -> std::optional<QString> override
{
- return QString(BuildConfig.MODRINTH_PROD_URL +
- "/project/%1/version?"
- "game_versions=[%2]&"
- "loaders=[\"%3\"]")
- .arg(args.addonId,
- getGameVersionsString(args.mcVersions),
- getModLoaderStrings(args.loaders).join("\",\""));
+ QStringList get_arguments;
+ if (args.mcVersions.has_value())
+ get_arguments.append(QString("game_versions=[%1]").arg(getGameVersionsString(args.mcVersions.value())));
+ if (args.loaders.has_value())
+ get_arguments.append(QString("loaders=[\"%1\"]").arg(getModLoaderStrings(args.loaders.value()).join("\",\"")));
+
+ return QString("%1/project/%2/version%3%4")
+ .arg(BuildConfig.MODRINTH_PROD_URL, args.pack.addonId.toString(), get_arguments.isEmpty() ? "" : "?", get_arguments.join('&'));
};
auto getGameVersionsArray(std::list<Version> mcVersions) const -> QString
@@ -127,12 +139,12 @@ class ModrinthAPI : public NetworkModAPI {
s += QString("\"versions:%1\",").arg(ver.toString());
}
s.remove(s.length() - 1, 1); //remove last comma
- return s.isEmpty() ? QString() : QString("[%1],").arg(s);
+ return s.isEmpty() ? QString() : s;
}
inline auto validateModLoaders(ModLoaderTypes loaders) const -> bool
{
- return (loaders == Unspecified) || (loaders & (Forge | Fabric | Quilt));
+ return loaders & (Forge | Fabric | Quilt);
}
};
diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp
index e2d27547..d1be7209 100644
--- a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp
+++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp
@@ -4,12 +4,15 @@
#include "Json.h"
-#include "ModDownloadTask.h"
+#include "ResourceDownloadTask.h"
#include "modplatform/helpers/HashUtils.h"
#include "tasks/ConcurrentTask.h"
+#include "minecraft/mod/ModFolderModel.h"
+#include "minecraft/mod/ResourceFolderModel.h"
+
static ModrinthAPI api;
static ModPlatform::ProviderCapabilities ProviderCaps;
@@ -34,7 +37,7 @@ void ModrinthCheckUpdate::executeTask()
// Create all hashes
QStringList hashes;
- auto best_hash_type = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH).first();
+ auto best_hash_type = ProviderCaps.hashType(ModPlatform::ResourceProvider::MODRINTH).first();
ConcurrentTask hashing_task(this, "MakeModrinthHashesTask", 10);
for (auto* mod : m_mods) {
@@ -108,11 +111,13 @@ void ModrinthCheckUpdate::executeTask()
// Sometimes a version may have multiple files, one with "forge" and one with "fabric",
// so we may want to filter it
QString loader_filter;
- static auto flags = { ModAPI::ModLoaderType::Forge, ModAPI::ModLoaderType::Fabric, ModAPI::ModLoaderType::Quilt };
- for (auto flag : flags) {
- if (m_loaders.testFlag(flag)) {
- loader_filter = api.getModLoaderString(flag);
- break;
+ if (m_loaders.has_value()) {
+ static auto flags = { ResourceAPI::ModLoaderType::Forge, ResourceAPI::ModLoaderType::Fabric, ResourceAPI::ModLoaderType::Quilt };
+ for (auto flag : flags) {
+ if (m_loaders.value().testFlag(flag)) {
+ loader_filter = api.getModLoaderString(flag);
+ break;
+ }
}
}
@@ -152,12 +157,12 @@ void ModrinthCheckUpdate::executeTask()
for (auto& author : mod->authors())
pack.authors.append({ author });
pack.description = mod->description();
- pack.provider = ModPlatform::Provider::MODRINTH;
+ pack.provider = ModPlatform::ResourceProvider::MODRINTH;
- auto download_task = new ModDownloadTask(pack, project_ver, m_mods_folder);
+ auto download_task = makeShared<ResourceDownloadTask>(pack, project_ver, m_mods_folder);
m_updatable.emplace_back(pack.name, hash, mod->version(), project_ver.version_number, project_ver.changelog,
- ModPlatform::Provider::MODRINTH, download_task);
+ ModPlatform::ResourceProvider::MODRINTH, download_task);
}
}
} catch (Json::JsonException& e) {
@@ -170,7 +175,7 @@ void ModrinthCheckUpdate::executeTask()
setStatus(tr("Waiting for the API response from Modrinth..."));
setProgress(1, 3);
- m_net_job = job.get();
+ m_net_job = qSharedPointerObjectCast<NetJob, Task>(job);
job->start();
lock.exec();
diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.h b/launcher/modplatform/modrinth/ModrinthCheckUpdate.h
index abf8ada1..88e1a675 100644
--- a/launcher/modplatform/modrinth/ModrinthCheckUpdate.h
+++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.h
@@ -8,7 +8,7 @@ class ModrinthCheckUpdate : public CheckUpdateTask {
Q_OBJECT
public:
- ModrinthCheckUpdate(QList<Mod*>& mods, std::list<Version>& mcVersions, ModAPI::ModLoaderTypes loaders, std::shared_ptr<ModFolderModel> mods_folder)
+ ModrinthCheckUpdate(QList<Mod*>& mods, std::list<Version>& mcVersions, std::optional<ResourceAPI::ModLoaderTypes> loaders, std::shared_ptr<ModFolderModel> mods_folder)
: CheckUpdateTask(mods, mcVersions, loaders, mods_folder)
{}
@@ -19,5 +19,5 @@ class ModrinthCheckUpdate : public CheckUpdateTask {
void executeTask() override;
private:
- NetJob* m_net_job = nullptr;
+ NetJob::Ptr m_net_job = nullptr;
};
diff --git a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp
index c5a27c9d..6814e645 100644
--- a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp
+++ b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp
@@ -223,12 +223,21 @@ bool ModrinthCreationTask::createInstance()
instance.setName(name());
instance.saveNow();
- m_files_job = new NetJob(tr("Mod download"), APPLICATION->network());
+ m_files_job.reset(new NetJob(tr("Mod download"), APPLICATION->network()));
+
+ auto root_modpack_path = FS::PathCombine(m_stagingPath, ".minecraft");
+ auto root_modpack_url = QUrl::fromLocalFile(root_modpack_path);
for (auto file : m_files) {
- auto path = FS::PathCombine(m_stagingPath, ".minecraft", file.path);
- qDebug() << "Will try to download" << file.downloads.front() << "to" << path;
- auto dl = Net::Download::makeFile(file.downloads.dequeue(), path);
+ auto file_path = FS::PathCombine(root_modpack_path, file.path);
+ if (!root_modpack_url.isParentOf(QUrl::fromLocalFile(file_path))) {
+ // This means we somehow got out of the root folder, so abort here to prevent exploits
+ setError(tr("One of the files has a path that leads to an arbitrary location (%1). This is a security risk and isn't allowed.").arg(file.path));
+ return false;
+ }
+
+ qDebug() << "Will try to download" << file.downloads.front() << "to" << file_path;
+ auto dl = Net::Download::makeFile(file.downloads.dequeue(), file_path);
dl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash));
m_files_job->addNetAction(dl);
@@ -236,8 +245,8 @@ bool ModrinthCreationTask::createInstance()
// FIXME: This really needs to be put into a ConcurrentTask of
// MultipleOptionsTask's , once those exist :)
auto param = dl.toWeakRef();
- connect(dl.get(), &NetAction::failed, [this, &file, path, param] {
- auto ndl = Net::Download::makeFile(file.downloads.dequeue(), path);
+ connect(dl.get(), &NetAction::failed, [this, &file, file_path, param] {
+ auto ndl = Net::Download::makeFile(file.downloads.dequeue(), file_path);
ndl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash));
m_files_job->addNetAction(ndl);
if (auto shared = param.lock()) shared->succeeded();
diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp
index aec45a73..7ade131e 100644
--- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp
+++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp
@@ -27,13 +27,14 @@
static ModrinthAPI api;
static ModPlatform::ProviderCapabilities ProviderCaps;
+// https://docs.modrinth.com/api-spec/#tag/projects/operation/getProject
void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj)
{
pack.addonId = Json::ensureString(obj, "project_id");
if (pack.addonId.toString().isEmpty())
pack.addonId = Json::requireString(obj, "id");
- pack.provider = ModPlatform::Provider::MODRINTH;
+ pack.provider = ModPlatform::ResourceProvider::MODRINTH;
pack.name = Json::requireString(obj, "title");
pack.slug = Json::ensureString(obj, "slug", "");
@@ -44,7 +45,7 @@ void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj)
pack.description = Json::ensureString(obj, "description", "");
- pack.logoUrl = Json::requireString(obj, "icon_url");
+ pack.logoUrl = Json::ensureString(obj, "icon_url", "");
pack.logoName = pack.addonId.toString();
ModPlatform::ModpackAuthor modAuthor;
@@ -95,10 +96,10 @@ void Modrinth::loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& ob
void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
QJsonArray& arr,
const shared_qobject_ptr<QNetworkAccessManager>& network,
- BaseInstance* inst)
+ const BaseInstance* inst)
{
QVector<ModPlatform::IndexedVersion> unsortedVersions;
- QString mcVersion = (static_cast<MinecraftInstance*>(inst))->getPackProfile()->getComponentVersion("net.minecraft");
+ QString mcVersion = (static_cast<const MinecraftInstance*>(inst))->getPackProfile()->getComponentVersion("net.minecraft");
for (auto versionIter : arr) {
auto obj = versionIter.toObject();
@@ -179,7 +180,7 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject& obj, QString preferred_hash_t
file.hash = Json::requireString(hash_list, preferred_hash_type);
file.hash_type = preferred_hash_type;
} else {
- auto hash_types = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH);
+ auto hash_types = ProviderCaps.hashType(ModPlatform::ResourceProvider::MODRINTH);
for (auto& hash_type : hash_types) {
if (hash_list.contains(hash_type)) {
file.hash = Json::requireString(hash_list, hash_type);
diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.h b/launcher/modplatform/modrinth/ModrinthPackIndex.h
index 31881414..e73e4b18 100644
--- a/launcher/modplatform/modrinth/ModrinthPackIndex.h
+++ b/launcher/modplatform/modrinth/ModrinthPackIndex.h
@@ -29,7 +29,7 @@ void loadExtraPackData(ModPlatform::IndexedPack& m, QJsonObject& obj);
void loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
QJsonArray& arr,
const shared_qobject_ptr<QNetworkAccessManager>& network,
- BaseInstance* inst);
+ const BaseInstance* inst);
auto loadIndexedPackVersion(QJsonObject& obj, QString hash_type = "sha512", QString filename_prefer = "") -> ModPlatform::IndexedVersion;
} // namespace Modrinth
diff --git a/launcher/modplatform/packwiz/Packwiz.cpp b/launcher/modplatform/packwiz/Packwiz.cpp
index 0ed29311..510c7309 100644
--- a/launcher/modplatform/packwiz/Packwiz.cpp
+++ b/launcher/modplatform/packwiz/Packwiz.cpp
@@ -97,7 +97,7 @@ auto V1::createModFormat(QDir& index_dir, ModPlatform::IndexedPack& mod_pack, Mo
mod.name = mod_pack.name;
mod.filename = mod_version.fileName;
- if (mod_pack.provider == ModPlatform::Provider::FLAME) {
+ if (mod_pack.provider == ModPlatform::ResourceProvider::FLAME) {
mod.mode = "metadata:curseforge";
} else {
mod.mode = "url";
@@ -176,11 +176,11 @@ void V1::updateModIndex(QDir& index_dir, Mod& mod)
in_stream << QString("\n[update]\n");
in_stream << QString("[update.%1]\n").arg(ProviderCaps.name(mod.provider));
switch (mod.provider) {
- case (ModPlatform::Provider::FLAME):
+ case (ModPlatform::ResourceProvider::FLAME):
in_stream << QString("file-id = %1\n").arg(mod.file_id.toString());
in_stream << QString("project-id = %1\n").arg(mod.project_id.toString());
break;
- case (ModPlatform::Provider::MODRINTH):
+ case (ModPlatform::ResourceProvider::MODRINTH):
addToStream("mod-id", mod.mod_id().toString());
addToStream("version", mod.version().toString());
break;
@@ -273,7 +273,7 @@ auto V1::getIndexForMod(QDir& index_dir, QString slug) -> Mod
}
{ // [update] info
- using Provider = ModPlatform::Provider;
+ using Provider = ModPlatform::ResourceProvider;
auto update_table = table["update"];
if (!update_table || !update_table.is_table()) {
diff --git a/launcher/modplatform/packwiz/Packwiz.h b/launcher/modplatform/packwiz/Packwiz.h
index 9754e5c4..4b096eec 100644
--- a/launcher/modplatform/packwiz/Packwiz.h
+++ b/launcher/modplatform/packwiz/Packwiz.h
@@ -49,7 +49,7 @@ class V1 {
QString hash {};
// [update]
- ModPlatform::Provider provider {};
+ ModPlatform::ResourceProvider provider {};
QVariant file_id {};
QVariant project_id {};
diff --git a/launcher/modplatform/technic/SingleZipPackInstallTask.cpp b/launcher/modplatform/technic/SingleZipPackInstallTask.cpp
index 6438d9ef..8fd43d21 100644
--- a/launcher/modplatform/technic/SingleZipPackInstallTask.cpp
+++ b/launcher/modplatform/technic/SingleZipPackInstallTask.cpp
@@ -44,7 +44,7 @@ void Technic::SingleZipPackInstallTask::executeTask()
const QString path = m_sourceUrl.host() + '/' + m_sourceUrl.path();
auto entry = APPLICATION->metacache()->resolveEntry("general", path);
entry->setStale(true);
- m_filesNetJob = new NetJob(tr("Modpack download"), APPLICATION->network());
+ m_filesNetJob.reset(new NetJob(tr("Modpack download"), APPLICATION->network()));
m_filesNetJob->addNetAction(Net::Download::makeCached(m_sourceUrl, entry));
m_archivePath = entry->getFullPath();
auto job = m_filesNetJob.get();
@@ -130,7 +130,7 @@ void Technic::SingleZipPackInstallTask::extractFinished()
}
}
- shared_qobject_ptr<Technic::TechnicPackProcessor> packProcessor = new Technic::TechnicPackProcessor();
+ auto packProcessor = makeShared<Technic::TechnicPackProcessor>();
connect(packProcessor.get(), &Technic::TechnicPackProcessor::succeeded, this, &Technic::SingleZipPackInstallTask::emitSucceeded);
connect(packProcessor.get(), &Technic::TechnicPackProcessor::failed, this, &Technic::SingleZipPackInstallTask::emitFailed);
packProcessor->run(m_globalSettings, name(), m_instIcon, m_stagingPath, m_minecraftVersion);
diff --git a/launcher/modplatform/technic/SolderPackInstallTask.cpp b/launcher/modplatform/technic/SolderPackInstallTask.cpp
index 19731b38..77c503f0 100644
--- a/launcher/modplatform/technic/SolderPackInstallTask.cpp
+++ b/launcher/modplatform/technic/SolderPackInstallTask.cpp
@@ -70,7 +70,7 @@ void Technic::SolderPackInstallTask::executeTask()
{
setStatus(tr("Resolving modpack files"));
- m_filesNetJob = new NetJob(tr("Resolving modpack files"), m_network);
+ m_filesNetJob.reset(new NetJob(tr("Resolving modpack files"), m_network));
auto sourceUrl = QString("%1/modpack/%2/%3").arg(m_solderUrl.toString(), m_pack, m_version);
m_filesNetJob->addNetAction(Net::Download::makeByteArray(sourceUrl, &m_response));
@@ -107,7 +107,7 @@ void Technic::SolderPackInstallTask::fileListSucceeded()
if (!build.minecraft.isEmpty())
m_minecraftVersion = build.minecraft;
- m_filesNetJob = new NetJob(tr("Downloading modpack"), m_network);
+ m_filesNetJob.reset(new NetJob(tr("Downloading modpack"), m_network));
int i = 0;
for (const auto &mod : build.mods) {
@@ -219,7 +219,7 @@ void Technic::SolderPackInstallTask::extractFinished()
}
}
- shared_qobject_ptr<Technic::TechnicPackProcessor> packProcessor = new Technic::TechnicPackProcessor();
+ auto packProcessor = makeShared<Technic::TechnicPackProcessor>();
connect(packProcessor.get(), &Technic::TechnicPackProcessor::succeeded, this, &Technic::SolderPackInstallTask::emitSucceeded);
connect(packProcessor.get(), &Technic::TechnicPackProcessor::failed, this, &Technic::SolderPackInstallTask::emitFailed);
packProcessor->run(m_globalSettings, name(), m_instIcon, m_stagingPath, m_minecraftVersion, true);
diff --git a/launcher/net/Download.cpp b/launcher/net/Download.cpp
index fd3dbedc..5982c8c9 100644
--- a/launcher/net/Download.cpp
+++ b/launcher/net/Download.cpp
@@ -49,14 +49,9 @@
namespace Net {
-Download::Download() : NetAction()
-{
- m_state = State::Inactive;
-}
-
auto Download::makeCached(QUrl url, MetaEntryPtr entry, Options options) -> Download::Ptr
{
- auto* dl = new Download();
+ auto dl = makeShared<Download>();
dl->m_url = url;
dl->m_options = options;
auto md5Node = new ChecksumValidator(QCryptographicHash::Md5);
@@ -67,7 +62,7 @@ auto Download::makeCached(QUrl url, MetaEntryPtr entry, Options options) -> Down
auto Download::makeByteArray(QUrl url, QByteArray* output, Options options) -> Download::Ptr
{
- auto* dl = new Download();
+ auto dl = makeShared<Download>();
dl->m_url = url;
dl->m_options = options;
dl->m_sink.reset(new ByteArraySink(output));
@@ -76,7 +71,7 @@ auto Download::makeByteArray(QUrl url, QByteArray* output, Options options) -> D
auto Download::makeFile(QUrl url, QString path, Options options) -> Download::Ptr
{
- auto* dl = new Download();
+ auto dl = makeShared<Download>();
dl->m_url = url;
dl->m_options = options;
dl->m_sink.reset(new FileSink(path));
diff --git a/launcher/net/Download.h b/launcher/net/Download.h
index 3faa5db5..7e1df322 100644
--- a/launcher/net/Download.h
+++ b/launcher/net/Download.h
@@ -52,9 +52,6 @@ class Download : public NetAction {
enum class Option { NoOptions = 0, AcceptLocalFiles = 1, MakeEternal = 2 };
Q_DECLARE_FLAGS(Options, Option)
- protected:
- explicit Download();
-
public:
~Download() override = default;
diff --git a/launcher/net/MetaCacheSink.cpp b/launcher/net/MetaCacheSink.cpp
index 5ae53c1c..c730fdbf 100644
--- a/launcher/net/MetaCacheSink.cpp
+++ b/launcher/net/MetaCacheSink.cpp
@@ -36,6 +36,7 @@
#include "MetaCacheSink.h"
#include <QFile>
#include <QFileInfo>
+#include <QRegularExpression>
#include "Application.h"
namespace Net {
diff --git a/launcher/net/NetAction.h b/launcher/net/NetAction.h
index d9c4fadc..38fe058b 100644
--- a/launcher/net/NetAction.h
+++ b/launcher/net/NetAction.h
@@ -52,7 +52,6 @@ class NetAction : public Task {
virtual ~NetAction() = default;
QUrl url() { return m_url; }
- auto index() -> int { return m_index_within_job; }
void setNetwork(shared_qobject_ptr<QNetworkAccessManager> network) { m_network = network; }
@@ -75,9 +74,6 @@ class NetAction : public Task {
public:
shared_qobject_ptr<QNetworkAccessManager> m_network;
- /// index within the parent job, FIXME: nuke
- int m_index_within_job = 0;
-
/// the network reply
unique_qobject_ptr<QNetworkReply> m_reply;
diff --git a/launcher/net/NetJob.cpp b/launcher/net/NetJob.cpp
index 9b5d4f1b..4bcd40b5 100644
--- a/launcher/net/NetJob.cpp
+++ b/launcher/net/NetJob.cpp
@@ -38,11 +38,10 @@
auto NetJob::addNetAction(NetAction::Ptr action) -> bool
{
- action->m_index_within_job = m_queue.size();
- m_queue.append(action);
-
action->setNetwork(m_network);
+ addTask(action);
+
return true;
}
diff --git a/launcher/net/PasteUpload.cpp b/launcher/net/PasteUpload.cpp
index 76b86743..d9e785c5 100644
--- a/launcher/net/PasteUpload.cpp
+++ b/launcher/net/PasteUpload.cpp
@@ -41,9 +41,11 @@
#include <QDebug>
#include <QJsonObject>
+#include <QHttpPart>
#include <QJsonArray>
#include <QJsonDocument>
#include <QFile>
+#include <QUrlQuery>
std::array<PasteUpload::PasteTypeInfo, 4> PasteUpload::PasteTypes = {
{{"0x0.st", "https://0x0.st", ""},
diff --git a/launcher/net/Upload.cpp b/launcher/net/Upload.cpp
index f3b19022..79b6af8d 100644
--- a/launcher/net/Upload.cpp
+++ b/launcher/net/Upload.cpp
@@ -233,7 +233,7 @@ namespace Net {
}
Upload::Ptr Upload::makeByteArray(QUrl url, QByteArray *output, QByteArray m_post_data) {
- auto* up = new Upload();
+ auto up = makeShared<Upload>();
up->m_url = std::move(url);
up->m_sink.reset(new ByteArraySink(output));
up->m_post_data = std::move(m_post_data);
diff --git a/launcher/net/Upload.h b/launcher/net/Upload.h
index 7c194bbc..5a0b2e74 100644
--- a/launcher/net/Upload.h
+++ b/launcher/net/Upload.h
@@ -45,6 +45,8 @@ namespace Net {
Q_OBJECT
public:
+ using Ptr = shared_qobject_ptr<Upload>;
+
static Upload::Ptr makeByteArray(QUrl url, QByteArray *output, QByteArray m_post_data);
auto abort() -> bool override;
auto canAbort() const -> bool override { return true; };
diff --git a/launcher/news/NewsChecker.cpp b/launcher/news/NewsChecker.cpp
index 3b969732..1f1520d0 100644
--- a/launcher/news/NewsChecker.cpp
+++ b/launcher/news/NewsChecker.cpp
@@ -57,10 +57,10 @@ void NewsChecker::reloadNews()
qDebug() << "Reloading news.";
- NetJob* job = new NetJob("News RSS Feed", m_network);
+ NetJob::Ptr job{ new NetJob("News RSS Feed", m_network) };
job->addNetAction(Net::Download::makeByteArray(m_feedUrl, &newsData));
- QObject::connect(job, &NetJob::succeeded, this, &NewsChecker::rssDownloadFinished);
- QObject::connect(job, &NetJob::failed, this, &NewsChecker::rssDownloadFailed);
+ QObject::connect(job.get(), &NetJob::succeeded, this, &NewsChecker::rssDownloadFinished);
+ QObject::connect(job.get(), &NetJob::failed, this, &NewsChecker::rssDownloadFailed);
m_newsNetJob.reset(job);
job->start();
}
diff --git a/launcher/tasks/ConcurrentTask.cpp b/launcher/tasks/ConcurrentTask.cpp
index a890013e..3cc37b2a 100644
--- a/launcher/tasks/ConcurrentTask.cpp
+++ b/launcher/tasks/ConcurrentTask.cpp
@@ -110,14 +110,14 @@ void ConcurrentTask::startNext()
setStepStatus(next->isMultiStep() ? next->getStepStatus() : next->getStatus());
updateState();
+ QCoreApplication::processEvents();
+
QMetaObject::invokeMethod(next.get(), &Task::start, Qt::QueuedConnection);
// Allow going up the number of concurrent tasks in case of tasks being added in the middle of a running task.
- int num_starts = m_total_max_size - m_doing.size();
+ int num_starts = qMin(m_queue.size(), m_total_max_size - m_doing.size());
for (int i = 0; i < num_starts; i++)
QMetaObject::invokeMethod(this, &ConcurrentTask::startNext, Qt::QueuedConnection);
-
- QCoreApplication::processEvents();
}
void ConcurrentTask::subTaskSucceeded(Task::Ptr task)
diff --git a/launcher/tasks/ConcurrentTask.h b/launcher/tasks/ConcurrentTask.h
index b46919fb..d074d2e2 100644
--- a/launcher/tasks/ConcurrentTask.h
+++ b/launcher/tasks/ConcurrentTask.h
@@ -8,6 +8,8 @@
class ConcurrentTask : public Task {
Q_OBJECT
public:
+ using Ptr = shared_qobject_ptr<ConcurrentTask>;
+
explicit ConcurrentTask(QObject* parent = nullptr, QString task_name = "", int max_concurrent = 6);
~ConcurrentTask() override;
diff --git a/launcher/translations/TranslationsModel.cpp b/launcher/translations/TranslationsModel.cpp
index 38f48296..46db4804 100644
--- a/launcher/translations/TranslationsModel.cpp
+++ b/launcher/translations/TranslationsModel.cpp
@@ -670,7 +670,7 @@ void TranslationsModel::downloadIndex()
return;
}
qDebug() << "Downloading Translations Index...";
- d->m_index_job = new NetJob("Translations Index", APPLICATION->network());
+ d->m_index_job.reset(new NetJob("Translations Index", APPLICATION->network()));
MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("translations", "index_v2.json");
entry->setStale(true);
d->m_index_task = Net::Download::makeCached(QUrl(BuildConfig.TRANSLATIONS_BASE_URL + "index_v2.json"), entry);
@@ -722,7 +722,7 @@ void TranslationsModel::downloadTranslation(QString key)
dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawHash));
dl->setProgress(dl->getProgress(), lang->file_size);
- d->m_dl_job = new NetJob("Translation for " + key, APPLICATION->network());
+ d->m_dl_job.reset(new NetJob("Translation for " + key, APPLICATION->network()));
d->m_dl_job->addNetAction(dl);
connect(d->m_dl_job.get(), &NetJob::succeeded, this, &TranslationsModel::dlGood);
diff --git a/launcher/ui/GuiUtil.cpp b/launcher/ui/GuiUtil.cpp
index 29467c3c..930e088a 100644
--- a/launcher/ui/GuiUtil.cpp
+++ b/launcher/ui/GuiUtil.cpp
@@ -40,6 +40,7 @@
#include <QClipboard>
#include <QApplication>
#include <QFileDialog>
+#include <QStandardPaths>
#include "ui/dialogs/ProgressDialog.h"
#include "ui/dialogs/CustomMessageBox.h"
diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp
index ab80fb80..6d21f5ed 100644
--- a/launcher/ui/MainWindow.cpp
+++ b/launcher/ui/MainWindow.cpp
@@ -43,6 +43,7 @@
#include "FileSystem.h"
#include "MainWindow.h"
+#include "ui_MainWindow.h"
#include <QVariant>
#include <QUrl>
@@ -86,8 +87,7 @@
#include <net/Download.h>
#include <news/NewsChecker.h>
#include <tools/BaseProfiler.h>
-#include <updater/DownloadTask.h>
-#include <updater/UpdateChecker.h>
+#include <updater/ExternalUpdater.h>
#include <DesktopServices.h>
#include "InstanceWindow.h"
#include "InstancePageProvider.h"
@@ -102,23 +102,19 @@
#include "ui/dialogs/NewsDialog.h"
#include "ui/dialogs/ProgressDialog.h"
#include "ui/dialogs/AboutDialog.h"
-#include "ui/dialogs/VersionSelectDialog.h"
#include "ui/dialogs/CustomMessageBox.h"
#include "ui/dialogs/IconPickerDialog.h"
#include "ui/dialogs/CopyInstanceDialog.h"
-#include "ui/dialogs/UpdateDialog.h"
#include "ui/dialogs/EditAccountDialog.h"
#include "ui/dialogs/ExportInstanceDialog.h"
-#include "ui/dialogs/ImportResourcePackDialog.h"
+#include "ui/dialogs/ImportResourceDialog.h"
#include "ui/themes/ITheme.h"
#include "ui/themes/ThemeManager.h"
-#include <minecraft/mod/ResourcePackFolderModel.h>
-#include <minecraft/mod/tasks/LocalResourcePackParseTask.h>
-#include <minecraft/mod/TexturePackFolderModel.h>
-#include <minecraft/mod/tasks/LocalTexturePackParseTask.h>
+#include "minecraft/mod/tasks/LocalResourceParse.h"
+#include "minecraft/mod/ModFolderModel.h"
+#include "minecraft/WorldList.h"
-#include "UpdateController.h"
#include "KonamiCode.h"
#include "InstanceImportTask.h"
@@ -140,785 +136,112 @@ QString profileInUseFilter(const QString & profile, bool used)
}
}
-// WHY: to hold the pre-translation strings together with the T pointer, so it can be retranslated without a lot of ugly code
-template <typename T>
-class Translated
+MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow)
{
-public:
- Translated(){}
- Translated(QWidget *parent)
- {
- m_contained = new T(parent);
- }
- void setTooltipId(const char * tooltip)
- {
- m_tooltip = tooltip;
- }
- void setTextId(const char * text)
- {
- m_text = text;
- }
- operator T*()
- {
- return m_contained;
- }
- T * operator->()
- {
- return m_contained;
- }
- void retranslate()
- {
- if(m_text)
- {
- QString result;
- result = QApplication::translate("MainWindow", m_text);
- if(result.contains("%1")) {
- result = result.arg(BuildConfig.LAUNCHER_DISPLAYNAME);
- }
- m_contained->setText(result);
- }
- if(m_tooltip)
- {
- QString result;
- result = QApplication::translate("MainWindow", m_tooltip);
- if(result.contains("%1")) {
- result = result.arg(BuildConfig.LAUNCHER_DISPLAYNAME);
- }
- m_contained->setToolTip(result);
- }
- }
-private:
- T * m_contained = nullptr;
- const char * m_text = nullptr;
- const char * m_tooltip = nullptr;
-};
-using TranslatedAction = Translated<QAction>;
-using TranslatedToolButton = Translated<QToolButton>;
-
-class TranslatedToolbar
-{
-public:
- TranslatedToolbar(){}
- TranslatedToolbar(QWidget *parent)
- {
- m_contained = new QToolBar(parent);
- }
- void setWindowTitleId(const char * title)
- {
- m_title = title;
- }
- operator QToolBar*()
- {
- return m_contained;
- }
- QToolBar * operator->()
- {
- return m_contained;
- }
- void retranslate()
- {
- if(m_title)
- {
- m_contained->setWindowTitle(QApplication::translate("MainWindow", m_title));
- }
- }
-private:
- QToolBar * m_contained = nullptr;
- const char * m_title = nullptr;
-};
-
-class MainWindow::Ui
-{
-public:
- TranslatedAction actionAddInstance;
- //TranslatedAction actionRefresh;
- TranslatedAction actionCheckUpdate;
- TranslatedAction actionSettings;
- TranslatedAction actionMoreNews;
- TranslatedAction actionManageAccounts;
- TranslatedAction actionLaunchInstance;
- TranslatedAction actionKillInstance;
- TranslatedAction actionRenameInstance;
- TranslatedAction actionChangeInstGroup;
- TranslatedAction actionChangeInstIcon;
- TranslatedAction actionEditInstance;
- TranslatedAction actionViewSelectedInstFolder;
- TranslatedAction actionDeleteInstance;
- TranslatedAction actionCAT;
- TranslatedAction actionCopyInstance;
- TranslatedAction actionLaunchInstanceOffline;
- TranslatedAction actionLaunchInstanceDemo;
- TranslatedAction actionExportInstance;
- TranslatedAction actionCreateInstanceShortcut;
- QVector<TranslatedAction *> all_actions;
-
- LabeledToolButton *renameButton = nullptr;
- LabeledToolButton *changeIconButton = nullptr;
-
- QMenu * foldersMenu = nullptr;
- TranslatedToolButton foldersMenuButton;
- TranslatedAction actionViewInstanceFolder;
- TranslatedAction actionViewCentralModsFolder;
-
- QMenu * editMenu = nullptr;
- TranslatedAction actionUndoTrashInstance;
-
- QMenu * helpMenu = nullptr;
- TranslatedToolButton helpMenuButton;
- TranslatedAction actionClearMetadata;
- #ifdef Q_OS_MAC
- TranslatedAction actionAddToPATH;
- #endif
- TranslatedAction actionReportBug;
- TranslatedAction actionDISCORD;
- TranslatedAction actionMATRIX;
- TranslatedAction actionREDDIT;
- TranslatedAction actionAbout;
-
- TranslatedAction actionNoAccountsAdded;
- TranslatedAction actionNoDefaultAccount;
-
- TranslatedAction actionLockToolbars;
-
- TranslatedAction actionChangeTheme;
-
- QVector<TranslatedToolButton *> all_toolbuttons;
-
- QWidget *centralWidget = nullptr;
- QHBoxLayout *horizontalLayout = nullptr;
- QStatusBar *statusBar = nullptr;
-
- QMenuBar *menuBar = nullptr;
- QMenu *fileMenu;
- QMenu *viewMenu;
- QMenu *profileMenu;
-
- TranslatedAction actionCloseWindow;
-
- TranslatedAction actionOpenWiki;
- TranslatedAction actionNewsMenuBar;
-
- TranslatedToolbar mainToolBar;
- TranslatedToolbar instanceToolBar;
- TranslatedToolbar newsToolBar;
- QVector<TranslatedToolbar *> all_toolbars;
-
- void createMainToolbarActions(MainWindow *MainWindow)
- {
- actionAddInstance = TranslatedAction(MainWindow);
- actionAddInstance->setObjectName(QStringLiteral("actionAddInstance"));
- actionAddInstance->setIcon(APPLICATION->getThemedIcon("new"));
- actionAddInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Add Instanc&e..."));
- actionAddInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Add a new instance."));
- actionAddInstance->setShortcut(QKeySequence::New);
- all_actions.append(&actionAddInstance);
-
- actionViewInstanceFolder = TranslatedAction(MainWindow);
- actionViewInstanceFolder->setObjectName(QStringLiteral("actionViewInstanceFolder"));
- actionViewInstanceFolder->setIcon(APPLICATION->getThemedIcon("viewfolder"));
- actionViewInstanceFolder.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&View Instance Folder"));
- actionViewInstanceFolder.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the instance folder in a file browser."));
- all_actions.append(&actionViewInstanceFolder);
-
- actionViewCentralModsFolder = TranslatedAction(MainWindow);
- actionViewCentralModsFolder->setObjectName(QStringLiteral("actionViewCentralModsFolder"));
- actionViewCentralModsFolder->setIcon(APPLICATION->getThemedIcon("centralmods"));
- actionViewCentralModsFolder.setTextId(QT_TRANSLATE_NOOP("MainWindow", "View &Central Mods Folder"));
- actionViewCentralModsFolder.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the central mods folder in a file browser."));
- all_actions.append(&actionViewCentralModsFolder);
-
- foldersMenu = new QMenu(MainWindow);
- foldersMenu->setTitle(tr("F&olders"));
- foldersMenu->setToolTipsVisible(true);
-
- foldersMenu->addAction(actionViewInstanceFolder);
- foldersMenu->addAction(actionViewCentralModsFolder);
-
- foldersMenuButton = TranslatedToolButton(MainWindow);
- foldersMenuButton.setTextId(QT_TRANSLATE_NOOP("MainWindow", "F&olders"));
- foldersMenuButton.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open one of the folders shared between instances."));
- foldersMenuButton->setMenu(foldersMenu);
- foldersMenuButton->setPopupMode(QToolButton::InstantPopup);
- foldersMenuButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
- foldersMenuButton->setIcon(APPLICATION->getThemedIcon("viewfolder"));
- foldersMenuButton->setFocusPolicy(Qt::NoFocus);
- all_toolbuttons.append(&foldersMenuButton);
-
- actionSettings = TranslatedAction(MainWindow);
- actionSettings->setObjectName(QStringLiteral("actionSettings"));
- actionSettings->setIcon(APPLICATION->getThemedIcon("settings"));
- actionSettings->setMenuRole(QAction::PreferencesRole);
- actionSettings.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Setti&ngs..."));
- actionSettings.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Change settings."));
- actionSettings->setShortcut(QKeySequence::Preferences);
- all_actions.append(&actionSettings);
-
- actionUndoTrashInstance = TranslatedAction(MainWindow);
- actionUndoTrashInstance->setObjectName(QStringLiteral("actionUndoTrashInstance"));
- actionUndoTrashInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&Undo Last Instance Deletion"));
- actionUndoTrashInstance->setEnabled(APPLICATION->instances()->trashedSomething());
- actionUndoTrashInstance->setShortcut(QKeySequence::Undo);
- all_actions.append(&actionUndoTrashInstance);
-
- actionClearMetadata = TranslatedAction(MainWindow);
- actionClearMetadata->setObjectName(QStringLiteral("actionClearMetadata"));
- actionClearMetadata->setIcon(APPLICATION->getThemedIcon("refresh"));
- actionClearMetadata.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&Clear Metadata Cache"));
- actionClearMetadata.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Clear cached metadata"));
- all_actions.append(&actionClearMetadata);
-
- #ifdef Q_OS_MAC
- actionAddToPATH = TranslatedAction(MainWindow);
- actionAddToPATH->setObjectName(QStringLiteral("actionAddToPATH"));
- actionAddToPATH.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Install to &PATH"));
- actionAddToPATH.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Install a prismlauncher symlink to /usr/local/bin"));
- all_actions.append(&actionAddToPATH);
- #endif
-
- if (!BuildConfig.BUG_TRACKER_URL.isEmpty()) {
- actionReportBug = TranslatedAction(MainWindow);
- actionReportBug->setObjectName(QStringLiteral("actionReportBug"));
- actionReportBug->setIcon(APPLICATION->getThemedIcon("bug"));
- actionReportBug.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Report a &Bug..."));
- actionReportBug.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the bug tracker to report a bug with %1."));
- all_actions.append(&actionReportBug);
- }
-
- if(!BuildConfig.MATRIX_URL.isEmpty()) {
- actionMATRIX = TranslatedAction(MainWindow);
- actionMATRIX->setObjectName(QStringLiteral("actionMATRIX"));
- actionMATRIX->setIcon(APPLICATION->getThemedIcon("matrix"));
- actionMATRIX.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&Matrix Space"));
- actionMATRIX.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open %1 Matrix space"));
- all_actions.append(&actionMATRIX);
- }
-
- if (!BuildConfig.DISCORD_URL.isEmpty()) {
- actionDISCORD = TranslatedAction(MainWindow);
- actionDISCORD->setObjectName(QStringLiteral("actionDISCORD"));
- actionDISCORD->setIcon(APPLICATION->getThemedIcon("discord"));
- actionDISCORD.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&Discord Guild"));
- actionDISCORD.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open %1 Discord guild."));
- all_actions.append(&actionDISCORD);
- }
-
- if (!BuildConfig.SUBREDDIT_URL.isEmpty()) {
- actionREDDIT = TranslatedAction(MainWindow);
- actionREDDIT->setObjectName(QStringLiteral("actionREDDIT"));
- actionREDDIT->setIcon(APPLICATION->getThemedIcon("reddit-alien"));
- actionREDDIT.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Sub&reddit"));
- actionREDDIT.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open %1 subreddit."));
- all_actions.append(&actionREDDIT);
- }
-
- actionAbout = TranslatedAction(MainWindow);
- actionAbout->setObjectName(QStringLiteral("actionAbout"));
- actionAbout->setIcon(APPLICATION->getThemedIcon("about"));
- actionAbout->setMenuRole(QAction::AboutRole);
- actionAbout.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&About %1"));
- actionAbout.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "View information about %1."));
- all_actions.append(&actionAbout);
-
- if(BuildConfig.UPDATER_ENABLED)
- {
- actionCheckUpdate = TranslatedAction(MainWindow);
- actionCheckUpdate->setObjectName(QStringLiteral("actionCheckUpdate"));
- actionCheckUpdate->setIcon(APPLICATION->getThemedIcon("checkupdate"));
- actionCheckUpdate.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&Update..."));
- actionCheckUpdate.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Check for new updates for %1."));
- actionCheckUpdate->setMenuRole(QAction::ApplicationSpecificRole);
- all_actions.append(&actionCheckUpdate);
- }
-
- actionCAT = TranslatedAction(MainWindow);
- actionCAT->setObjectName(QStringLiteral("actionCAT"));
- actionCAT->setCheckable(true);
- actionCAT->setIcon(APPLICATION->getThemedIcon("cat"));
- actionCAT.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&Meow"));
- actionCAT.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "It's a fluffy kitty :3"));
- actionCAT->setPriority(QAction::LowPriority);
- all_actions.append(&actionCAT);
-
- // profile menu and its actions
- actionManageAccounts = TranslatedAction(MainWindow);
- actionManageAccounts->setObjectName(QStringLiteral("actionManageAccounts"));
- actionManageAccounts.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&Manage Accounts..."));
- // FIXME: no tooltip!
- 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);
-
- actionChangeTheme = TranslatedAction(MainWindow);
- actionChangeTheme->setObjectName(QStringLiteral("actionChangeTheme"));
- actionChangeTheme.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Themes"));
- all_actions.append(&actionChangeTheme);
- }
-
- void createMainToolbar(QMainWindow *MainWindow)
- {
- mainToolBar = TranslatedToolbar(MainWindow);
- mainToolBar->setVisible(menuBar->isNativeMenuBar() || !APPLICATION->settings()->get("MenuBarInsteadOfToolBar").toBool());
- mainToolBar->setObjectName(QStringLiteral("mainToolBar"));
- mainToolBar->setAllowedAreas(Qt::TopToolBarArea | Qt::BottomToolBarArea);
- mainToolBar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
- mainToolBar->setFloatable(false);
- mainToolBar.setWindowTitleId(QT_TRANSLATE_NOOP("MainWindow", "Main Toolbar"));
-
- mainToolBar->addAction(actionAddInstance);
-
- mainToolBar->addSeparator();
-
- QWidgetAction* foldersButtonAction = new QWidgetAction(MainWindow);
- foldersButtonAction->setDefaultWidget(foldersMenuButton);
- mainToolBar->addAction(foldersButtonAction);
-
- mainToolBar->addAction(actionSettings);
-
- helpMenu = new QMenu(MainWindow);
- helpMenu->setToolTipsVisible(true);
-
- helpMenu->addAction(actionClearMetadata);
-
- #ifdef Q_OS_MAC
- helpMenu->addAction(actionAddToPATH);
- #endif
-
- if (!BuildConfig.BUG_TRACKER_URL.isEmpty()) {
- helpMenu->addAction(actionReportBug);
- }
-
- if(!BuildConfig.MATRIX_URL.isEmpty()) {
- helpMenu->addAction(actionMATRIX);
- }
-
- if (!BuildConfig.DISCORD_URL.isEmpty()) {
- helpMenu->addAction(actionDISCORD);
- }
-
- if (!BuildConfig.SUBREDDIT_URL.isEmpty()) {
- helpMenu->addAction(actionREDDIT);
- }
-
- helpMenu->addAction(actionAbout);
-
- helpMenuButton = TranslatedToolButton(MainWindow);
- helpMenuButton.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Help"));
- helpMenuButton.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Get help with %1 or Minecraft."));
- helpMenuButton->setMenu(helpMenu);
- helpMenuButton->setPopupMode(QToolButton::InstantPopup);
- helpMenuButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
- helpMenuButton->setIcon(APPLICATION->getThemedIcon("help"));
- helpMenuButton->setFocusPolicy(Qt::NoFocus);
- all_toolbuttons.append(&helpMenuButton);
- QWidgetAction* helpButtonAction = new QWidgetAction(MainWindow);
- helpButtonAction->setDefaultWidget(helpMenuButton);
- mainToolBar->addAction(helpButtonAction);
-
- if(BuildConfig.UPDATER_ENABLED)
- {
- mainToolBar->addAction(actionCheckUpdate);
- }
-
- mainToolBar->addSeparator();
-
- mainToolBar->addAction(actionCAT);
-
- all_toolbars.append(&mainToolBar);
- MainWindow->addToolBar(Qt::TopToolBarArea, mainToolBar);
- }
-
- void createMenuBar(QMainWindow *MainWindow)
- {
- menuBar = new QMenuBar(MainWindow);
- menuBar->setVisible(APPLICATION->settings()->get("MenuBarInsteadOfToolBar").toBool());
-
- fileMenu = menuBar->addMenu(tr("&File"));
- // Workaround for QTBUG-94802 (https://bugreports.qt.io/browse/QTBUG-94802); also present for other menus
- fileMenu->setSeparatorsCollapsible(false);
- fileMenu->addAction(actionAddInstance);
- fileMenu->addAction(actionLaunchInstance);
- fileMenu->addAction(actionKillInstance);
- fileMenu->addAction(actionCloseWindow);
- fileMenu->addSeparator();
- fileMenu->addAction(actionEditInstance);
- fileMenu->addAction(actionChangeInstGroup);
- fileMenu->addAction(actionViewSelectedInstFolder);
- fileMenu->addAction(actionExportInstance);
- fileMenu->addAction(actionCopyInstance);
- fileMenu->addAction(actionDeleteInstance);
- fileMenu->addAction(actionCreateInstanceShortcut);
- fileMenu->addSeparator();
- fileMenu->addAction(actionSettings);
-
- editMenu = menuBar->addMenu(tr("&Edit"));
- editMenu->addAction(actionUndoTrashInstance);
-
- viewMenu = menuBar->addMenu(tr("&View"));
- viewMenu->setSeparatorsCollapsible(false);
- viewMenu->addAction(actionChangeTheme);
- viewMenu->addSeparator();
- viewMenu->addAction(actionCAT);
- viewMenu->addSeparator();
-
- viewMenu->addAction(actionLockToolbars);
-
- menuBar->addMenu(foldersMenu);
-
- profileMenu = menuBar->addMenu(tr("&Accounts"));
- profileMenu->setSeparatorsCollapsible(false);
- profileMenu->addAction(actionManageAccounts);
-
- helpMenu = menuBar->addMenu(tr("&Help"));
- helpMenu->setSeparatorsCollapsible(false);
- helpMenu->addAction(actionClearMetadata);
- #ifdef Q_OS_MAC
- helpMenu->addAction(actionAddToPATH);
- #endif
- helpMenu->addSeparator();
- helpMenu->addAction(actionAbout);
- helpMenu->addAction(actionOpenWiki);
- helpMenu->addAction(actionNewsMenuBar);
- helpMenu->addSeparator();
- if (!BuildConfig.BUG_TRACKER_URL.isEmpty())
- helpMenu->addAction(actionReportBug);
- if (!BuildConfig.MATRIX_URL.isEmpty())
- helpMenu->addAction(actionMATRIX);
- if (!BuildConfig.DISCORD_URL.isEmpty())
- helpMenu->addAction(actionDISCORD);
- if (!BuildConfig.SUBREDDIT_URL.isEmpty())
- helpMenu->addAction(actionREDDIT);
- if(BuildConfig.UPDATER_ENABLED)
- {
- helpMenu->addSeparator();
- helpMenu->addAction(actionCheckUpdate);
- }
- MainWindow->setMenuBar(menuBar);
- }
+ ui->setupUi(this);
- void createMenuActions(MainWindow *MainWindow)
- {
- actionCloseWindow = TranslatedAction(MainWindow);
- actionCloseWindow->setObjectName(QStringLiteral("actionCloseWindow"));
- actionCloseWindow.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Close &Window"));
- actionCloseWindow.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Close the current window"));
- actionCloseWindow->setShortcut(QKeySequence::Close);
- connect(actionCloseWindow, &QAction::triggered, APPLICATION, &Application::closeCurrentWindow);
- all_actions.append(&actionCloseWindow);
-
- actionOpenWiki = TranslatedAction(MainWindow);
- actionOpenWiki->setObjectName(QStringLiteral("actionOpenWiki"));
- actionOpenWiki.setTextId(QT_TRANSLATE_NOOP("MainWindow", "%1 &Help"));
- actionOpenWiki.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the %1 wiki"));
- actionOpenWiki->setIcon(APPLICATION->getThemedIcon("help"));
- connect(actionOpenWiki, &QAction::triggered, MainWindow, &MainWindow::on_actionOpenWiki_triggered);
- all_actions.append(&actionOpenWiki);
-
- actionNewsMenuBar = TranslatedAction(MainWindow);
- actionNewsMenuBar->setObjectName(QStringLiteral("actionNewsMenuBar"));
- actionNewsMenuBar.setTextId(QT_TRANSLATE_NOOP("MainWindow", "%1 &News"));
- actionNewsMenuBar.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the %1 wiki"));
- actionNewsMenuBar->setIcon(APPLICATION->getThemedIcon("news"));
- connect(actionNewsMenuBar, &QAction::triggered, MainWindow, &MainWindow::on_actionMoreNews_triggered);
- all_actions.append(&actionNewsMenuBar);
- }
-
- // "Instance actions" are actions that require an instance to be selected (i.e. "new instance" is not here)
- // Actions that also require other conditions (e.g. a running instance) won't be changed.
- void setInstanceActionsEnabled(bool enabled)
- {
- actionEditInstance->setEnabled(enabled);
- actionChangeInstGroup->setEnabled(enabled);
- actionViewSelectedInstFolder->setEnabled(enabled);
- actionExportInstance->setEnabled(enabled);
- actionDeleteInstance->setEnabled(enabled);
- actionCopyInstance->setEnabled(enabled);
- actionCreateInstanceShortcut->setEnabled(enabled);
- }
+ setWindowIcon(APPLICATION->getThemedIcon("logo"));
+ setWindowTitle(APPLICATION->applicationDisplayName());
+#ifndef QT_NO_ACCESSIBILITY
+ setAccessibleName(BuildConfig.LAUNCHER_DISPLAYNAME);
+#endif
- void createStatusBar(QMainWindow *MainWindow)
+ // instance toolbar stuff
{
- statusBar = new QStatusBar(MainWindow);
- statusBar->setObjectName(QStringLiteral("statusBar"));
- MainWindow->setStatusBar(statusBar);
- }
+ // Qt doesn't like vertical moving toolbars, so we have to force them...
+ // See https://github.com/PolyMC/PolyMC/issues/493
+ connect(ui->instanceToolBar, &QToolBar::orientationChanged,
+ [=](Qt::Orientation) { ui->instanceToolBar->setOrientation(Qt::Vertical); });
- void createNewsToolbar(QMainWindow *MainWindow)
- {
- newsToolBar = TranslatedToolbar(MainWindow);
- newsToolBar->setObjectName(QStringLiteral("newsToolBar"));
- newsToolBar->setAllowedAreas(Qt::TopToolBarArea | Qt::BottomToolBarArea);
- newsToolBar->setIconSize(QSize(16, 16));
- newsToolBar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
- newsToolBar->setFloatable(false);
- newsToolBar->setWindowTitle(QT_TRANSLATE_NOOP("MainWindow", "News Toolbar"));
-
- actionMoreNews = TranslatedAction(MainWindow);
- actionMoreNews->setObjectName(QStringLiteral("actionMoreNews"));
- actionMoreNews->setIcon(APPLICATION->getThemedIcon("news"));
- actionMoreNews.setTextId(QT_TRANSLATE_NOOP("MainWindow", "More news..."));
- actionMoreNews.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the development blog to read more news about %1."));
- all_actions.append(&actionMoreNews);
- newsToolBar->addAction(actionMoreNews);
-
- all_toolbars.append(&newsToolBar);
- MainWindow->addToolBar(Qt::BottomToolBarArea, newsToolBar);
- }
-
- void createInstanceActions(QMainWindow *MainWindow)
- {
- // NOTE: not added to toolbar, but used for instance context menu (right click)
- actionChangeInstIcon = TranslatedAction(MainWindow);
- actionChangeInstIcon->setObjectName(QStringLiteral("actionChangeInstIcon"));
- actionChangeInstIcon->setIcon(QIcon(":/icons/instances/grass"));
- actionChangeInstIcon->setIconVisibleInMenu(true);
- actionChangeInstIcon.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Change Icon"));
- actionChangeInstIcon.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Change the selected instance's icon."));
- all_actions.append(&actionChangeInstIcon);
-
- changeIconButton = new LabeledToolButton(MainWindow);
+ // if you try to add a widget to a toolbar in a .ui file
+ // qt designer will delete it when you save the file >:(
+ changeIconButton = new LabeledToolButton(this);
changeIconButton->setObjectName(QStringLiteral("changeIconButton"));
changeIconButton->setIcon(APPLICATION->getThemedIcon("news"));
- changeIconButton->setToolTip(actionChangeInstIcon->toolTip());
changeIconButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
+ connect(changeIconButton, &QToolButton::clicked, this, &MainWindow::on_actionChangeInstIcon_triggered);
+ ui->instanceToolBar->insertWidgetBefore(ui->actionLaunchInstance, changeIconButton);
- // NOTE: not added to toolbar, but used for instance context menu (right click)
- actionRenameInstance = TranslatedAction(MainWindow);
- actionRenameInstance->setObjectName(QStringLiteral("actionRenameInstance"));
- actionRenameInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Rename"));
- actionRenameInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Rename the selected instance."));
- actionRenameInstance->setIcon(APPLICATION->getThemedIcon("rename"));
- all_actions.append(&actionRenameInstance);
-
- // the rename label is inside the rename tool button
- renameButton = new LabeledToolButton(MainWindow);
+ renameButton = new LabeledToolButton(this);
renameButton->setObjectName(QStringLiteral("renameButton"));
- renameButton->setToolTip(actionRenameInstance->toolTip());
renameButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
+ connect(renameButton, &QToolButton::clicked, this, &MainWindow::on_actionRenameInstance_triggered);
+ ui->instanceToolBar->insertWidgetBefore(ui->actionLaunchInstance, renameButton);
- actionLaunchInstance = TranslatedAction(MainWindow);
- actionLaunchInstance->setObjectName(QStringLiteral("actionLaunchInstance"));
- actionLaunchInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&Launch"));
- actionLaunchInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Launch the selected instance."));
- actionLaunchInstance->setIcon(APPLICATION->getThemedIcon("launch"));
- all_actions.append(&actionLaunchInstance);
-
- actionLaunchInstanceOffline = TranslatedAction(MainWindow);
- actionLaunchInstanceOffline->setObjectName(QStringLiteral("actionLaunchInstanceOffline"));
- actionLaunchInstanceOffline.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Launch &Offline"));
- actionLaunchInstanceOffline.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Launch the selected instance in offline mode."));
- all_actions.append(&actionLaunchInstanceOffline);
-
- actionLaunchInstanceDemo = TranslatedAction(MainWindow);
- actionLaunchInstanceDemo->setObjectName(QStringLiteral("actionLaunchInstanceDemo"));
- actionLaunchInstanceDemo.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Launch &Demo"));
- actionLaunchInstanceDemo.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Launch the selected instance in demo mode."));
- all_actions.append(&actionLaunchInstanceDemo);
-
- actionKillInstance = TranslatedAction(MainWindow);
- actionKillInstance->setObjectName(QStringLiteral("actionKillInstance"));
- actionKillInstance->setDisabled(true);
- actionKillInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&Kill"));
- actionKillInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Kill the running instance"));
- actionKillInstance->setShortcut(QKeySequence(tr("Ctrl+K")));
- actionKillInstance->setIcon(APPLICATION->getThemedIcon("status-bad"));
- all_actions.append(&actionKillInstance);
-
- actionEditInstance = TranslatedAction(MainWindow);
- actionEditInstance->setObjectName(QStringLiteral("actionEditInstance"));
- actionEditInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&Edit..."));
- actionEditInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Change the instance settings, mods and versions."));
- actionEditInstance->setShortcut(QKeySequence(tr("Ctrl+I")));
- actionEditInstance->setIcon(APPLICATION->getThemedIcon("settings-configure"));
- all_actions.append(&actionEditInstance);
-
- actionChangeInstGroup = TranslatedAction(MainWindow);
- actionChangeInstGroup->setObjectName(QStringLiteral("actionChangeInstGroup"));
- actionChangeInstGroup.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&Change Group..."));
- actionChangeInstGroup.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Change the selected instance's group."));
- actionChangeInstGroup->setShortcut(QKeySequence(tr("Ctrl+G")));
- actionChangeInstGroup->setIcon(APPLICATION->getThemedIcon("tag"));
- all_actions.append(&actionChangeInstGroup);
-
- actionViewSelectedInstFolder = TranslatedAction(MainWindow);
- actionViewSelectedInstFolder->setObjectName(QStringLiteral("actionViewSelectedInstFolder"));
- actionViewSelectedInstFolder.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&Folder"));
- actionViewSelectedInstFolder.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the selected instance's root folder in a file browser."));
- actionViewSelectedInstFolder->setIcon(APPLICATION->getThemedIcon("viewfolder"));
- all_actions.append(&actionViewSelectedInstFolder);
-
- actionExportInstance = TranslatedAction(MainWindow);
- actionExportInstance->setObjectName(QStringLiteral("actionExportInstance"));
- actionExportInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "E&xport..."));
- actionExportInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Export the selected instance as a zip file."));
- actionExportInstance->setShortcut(QKeySequence(tr("Ctrl+E")));
- actionExportInstance->setIcon(APPLICATION->getThemedIcon("export"));
- all_actions.append(&actionExportInstance);
-
- actionDeleteInstance = TranslatedAction(MainWindow);
- actionDeleteInstance->setObjectName(QStringLiteral("actionDeleteInstance"));
- actionDeleteInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Dele&te"));
- actionDeleteInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Delete the selected instance."));
- actionDeleteInstance->setShortcuts({QKeySequence(tr("Backspace")), QKeySequence::Delete});
- actionDeleteInstance->setAutoRepeat(false);
- actionDeleteInstance->setIcon(APPLICATION->getThemedIcon("delete"));
- all_actions.append(&actionDeleteInstance);
-
- actionCopyInstance = TranslatedAction(MainWindow);
- actionCopyInstance->setObjectName(QStringLiteral("actionCopyInstance"));
- actionCopyInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Cop&y..."));
- actionCopyInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Copy the selected instance."));
- actionCopyInstance->setShortcut(QKeySequence(tr("Ctrl+D")));
- actionCopyInstance->setIcon(APPLICATION->getThemedIcon("copy"));
- all_actions.append(&actionCopyInstance);
-
- actionCreateInstanceShortcut = TranslatedAction(MainWindow);
- actionCreateInstanceShortcut->setObjectName(QStringLiteral("actionCreateInstanceShortcut"));
- actionCreateInstanceShortcut.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Create Shortcut"));
- actionCreateInstanceShortcut.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Creates a shortcut on your desktop to launch the selected instance."));
- actionCreateInstanceShortcut->setIcon(APPLICATION->getThemedIcon("shortcut"));
- all_actions.append(&actionCreateInstanceShortcut);
-
- setInstanceActionsEnabled(false);
- }
-
- void createInstanceToolbar(QMainWindow *MainWindow)
- {
- instanceToolBar = TranslatedToolbar(MainWindow);
- instanceToolBar->setObjectName(QStringLiteral("instanceToolBar"));
- // disabled until we have an instance selected
- instanceToolBar->setEnabled(false);
- // 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); });
- instanceToolBar->setAllowedAreas(Qt::LeftToolBarArea | Qt::RightToolBarArea);
- instanceToolBar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
- instanceToolBar->setIconSize(QSize(16, 16));
-
- instanceToolBar->setFloatable(false);
- instanceToolBar->setWindowTitle(QT_TRANSLATE_NOOP("MainWindow", "Instance Toolbar"));
-
- instanceToolBar->addWidget(changeIconButton);
- instanceToolBar->addWidget(renameButton);
-
- instanceToolBar->addSeparator();
-
- instanceToolBar->addAction(actionLaunchInstance);
- instanceToolBar->addAction(actionKillInstance);
-
- instanceToolBar->addSeparator();
+ ui->instanceToolBar->insertSeparator(ui->actionLaunchInstance);
- instanceToolBar->addAction(actionEditInstance);
- instanceToolBar->addAction(actionChangeInstGroup);
-
- instanceToolBar->addAction(actionViewSelectedInstFolder);
-
- instanceToolBar->addAction(actionExportInstance);
- instanceToolBar->addAction(actionCopyInstance);
- instanceToolBar->addAction(actionDeleteInstance);
+ // restore the instance toolbar settings
+ auto const setting_name = QString("WideBarVisibility_%1").arg(ui->instanceToolBar->objectName());
+ if (!APPLICATION->settings()->contains(setting_name))
+ instanceToolbarSetting = APPLICATION->settings()->registerSetting(setting_name);
+ else
+ instanceToolbarSetting = APPLICATION->settings()->getSetting(setting_name);
- instanceToolBar->addAction(actionCreateInstanceShortcut); // TODO find better position for this
+ ui->instanceToolBar->setVisibilityState(instanceToolbarSetting->get().toByteArray());
- QLayout * lay = instanceToolBar->layout();
- for(int i = 0; i < lay->count(); i++)
- {
- QLayoutItem * item = lay->itemAt(i);
- if (item->widget()->metaObject()->className() == QString("QToolButton"))
- {
- item->setAlignment(Qt::AlignLeft);
- }
- }
+ ui->instanceToolBar->addContextMenuAction(ui->newsToolBar->toggleViewAction());
+ ui->instanceToolBar->addContextMenuAction(ui->instanceToolBar->toggleViewAction());
+ ui->instanceToolBar->addContextMenuAction(ui->actionLockToolbars);
- all_toolbars.append(&instanceToolBar);
- MainWindow->addToolBar(Qt::RightToolBarArea, instanceToolBar);
}
- void setupUi(MainWindow *MainWindow)
+ // set the menu for the folders help, and accounts tool buttons
{
- if (MainWindow->objectName().isEmpty())
- {
- MainWindow->setObjectName(QStringLiteral("MainWindow"));
- }
- MainWindow->resize(800, 600);
- MainWindow->setWindowIcon(APPLICATION->getThemedIcon("logo"));
- MainWindow->setWindowTitle(APPLICATION->applicationDisplayName());
-#ifndef QT_NO_ACCESSIBILITY
- MainWindow->setAccessibleName(BuildConfig.LAUNCHER_DISPLAYNAME);
-#endif
-
- createMainToolbarActions(MainWindow);
- createMenuActions(MainWindow);
- createInstanceActions(MainWindow);
-
- createMenuBar(MainWindow);
-
- createMainToolbar(MainWindow);
-
- centralWidget = new QWidget(MainWindow);
- centralWidget->setObjectName(QStringLiteral("centralWidget"));
- horizontalLayout = new QHBoxLayout(centralWidget);
- horizontalLayout->setSpacing(0);
- horizontalLayout->setObjectName(QStringLiteral("horizontalLayout"));
- horizontalLayout->setSizeConstraint(QLayout::SetDefaultConstraint);
- horizontalLayout->setContentsMargins(0, 0, 0, 0);
- MainWindow->setCentralWidget(centralWidget);
+ auto foldersMenuButton = dynamic_cast<QToolButton*>(ui->mainToolBar->widgetForAction(ui->actionFoldersButton));
+ ui->actionFoldersButton->setMenu(ui->foldersMenu);
+ foldersMenuButton->setPopupMode(QToolButton::InstantPopup);
- createStatusBar(MainWindow);
- createNewsToolbar(MainWindow);
- createInstanceToolbar(MainWindow);
+ helpMenuButton = dynamic_cast<QToolButton*>(ui->mainToolBar->widgetForAction(ui->actionHelpButton));
+ ui->actionHelpButton->setMenu(new QMenu(this));
+ ui->actionHelpButton->menu()->addActions(ui->helpMenu->actions());
+ ui->actionHelpButton->menu()->removeAction(ui->actionCheckUpdate);
+ helpMenuButton->setPopupMode(QToolButton::InstantPopup);
- MainWindow->updateToolsMenu();
- MainWindow->updateThemeMenu();
+ auto accountMenuButton = dynamic_cast<QToolButton*>(ui->mainToolBar->widgetForAction(ui->actionAccountsButton));
+ ui->actionAccountsButton->setMenu(ui->accountsMenu);
+ accountMenuButton->setPopupMode(QToolButton::InstantPopup);
+ }
- retranslateUi(MainWindow);
+ // hide, disable and show stuff
+ {
+ ui->actionReportBug->setVisible(!BuildConfig.BUG_TRACKER_URL.isEmpty());
+ ui->actionMATRIX->setVisible(!BuildConfig.MATRIX_URL.isEmpty());
+ ui->actionDISCORD->setVisible(!BuildConfig.DISCORD_URL.isEmpty());
+ ui->actionREDDIT->setVisible(!BuildConfig.SUBREDDIT_URL.isEmpty());
- QMetaObject::connectSlotsByName(MainWindow);
- } // setupUi
+ ui->actionCheckUpdate->setVisible(BuildConfig.UPDATER_ENABLED);
- void retranslateUi(MainWindow *MainWindow)
- {
- // all the actions
- for(auto * item: all_actions)
- {
- item->retranslate();
- }
- for(auto * item: all_toolbars)
- {
- item->retranslate();
- }
- for(auto * item: all_toolbuttons)
- {
- item->retranslate();
- }
- // submenu buttons
- foldersMenuButton->setText(tr("Folders"));
- helpMenuButton->setText(tr("Help"));
+#ifndef Q_OS_MAC
+ ui->actionAddToPATH->setVisible(false);
+#endif
- // playtime counter
- if (MainWindow->m_statusCenter)
- {
- MainWindow->updateStatusCenter();
- }
- } // retranslateUi
-};
+ // disabled until we have an instance selected
+ ui->instanceToolBar->setEnabled(false);
+ setInstanceActionsEnabled(false);
+ }
-MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow::Ui)
-{
- ui->setupUi(this);
+ // add the toolbar toggles to the view menu
+ ui->viewMenu->addAction(ui->instanceToolBar->toggleViewAction());
+ ui->viewMenu->addAction(ui->newsToolBar->toggleViewAction());
+ updateThemeMenu();
+ updateMainToolBar();
// OSX magic.
setUnifiedTitleAndToolBarOnMac(true);
// Global shortcuts
{
+ // you can't set QKeySequence::StandardKey shortcuts in qt designer >:(
+ ui->actionAddInstance->setShortcut(QKeySequence::New);
+ ui->actionSettings->setShortcut(QKeySequence::Preferences);
+ ui->actionUndoTrashInstance->setShortcut(QKeySequence::Undo);
+ ui->actionDeleteInstance->setShortcuts({ QKeySequence(tr("Backspace")), QKeySequence::Delete });
+ ui->actionCloseWindow->setShortcut(QKeySequence::Close);
+ connect(ui->actionCloseWindow, &QAction::triggered, APPLICATION, &Application::closeCurrentWindow);
+
// FIXME: This is kinda weird. and bad. We need some kind of managed shutdown.
auto q = new QShortcut(QKeySequence::Quit, this);
- connect(q, SIGNAL(activated()), qApp, SLOT(quit()));
+ connect(q, &QShortcut::activated, APPLICATION, &Application::quit);
}
// Konami Code
@@ -936,6 +259,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow
newsLabel->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
newsLabel->setFocusPolicy(Qt::NoFocus);
ui->newsToolBar->insertWidget(ui->actionMoreNews, newsLabel);
+
QObject::connect(newsLabel, &QAbstractButton::clicked, this, &MainWindow::newsButtonClicked);
QObject::connect(m_newsChecker.get(), &NewsChecker::newsLoaded, this, &MainWindow::updateNewsLabel);
updateNewsLabel();
@@ -955,7 +279,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow
view->installEventFilter(this);
view->setContextMenuPolicy(Qt::CustomContextMenu);
connect(view, &QWidget::customContextMenuRequested, this, &MainWindow::showInstanceContextMenu);
- connect(view, &InstanceView::droppedURLs, this, &MainWindow::droppedURLs, Qt::QueuedConnection);
+ connect(view, &InstanceView::droppedURLs, this, &MainWindow::processURLs, Qt::QueuedConnection);
proxymodel = new InstanceProxyModel(this);
proxymodel->setSourceModel(APPLICATION->instances().get());
@@ -971,10 +295,12 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow
}
// The cat background
{
+ // set the cat action priority here so you can still see the action in qt designer
+ ui->actionCAT->setPriority(QAction::LowPriority);
bool cat_enable = APPLICATION->settings()->get("TheCat").toBool();
ui->actionCAT->setChecked(cat_enable);
- // NOTE: calling the operator like that is an ugly hack to appease ancient gcc...
- connect(ui->actionCAT.operator->(), SIGNAL(toggled(bool)), SLOT(onCatToggled(bool)));
+ connect(ui->actionCAT, &QAction::toggled, this, &MainWindow::onCatToggled);
+ connect(APPLICATION, &Application::currentCatChanged, this, &MainWindow::onCatChanged);
setCatBackground(cat_enable);
}
@@ -1011,25 +337,13 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow
// Add "manage accounts" button, right align
QWidget *spacer = new QWidget();
spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
- ui->mainToolBar->addWidget(spacer);
+ ui->mainToolBar->insertWidget(ui->actionAccountsButton, spacer);
- accountMenu = new QMenu(this);
// Use undocumented property... https://stackoverflow.com/questions/7121718/create-a-scrollbar-in-a-submenu-qt
- accountMenu->setStyleSheet("QMenu { menu-scrollable: 1; }");
+ ui->accountsMenu->setStyleSheet("QMenu { menu-scrollable: 1; }");
repopulateAccountsMenu();
- accountMenuButton = new QToolButton(this);
- accountMenuButton->setMenu(accountMenu);
- accountMenuButton->setPopupMode(QToolButton::InstantPopup);
- accountMenuButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
- accountMenuButton->setIcon(APPLICATION->getThemedIcon("noaccount"));
-
- QWidgetAction *accountMenuButtonAction = new QWidgetAction(this);
- accountMenuButtonAction->setDefaultWidget(accountMenuButton);
-
- ui->mainToolBar->addAction(accountMenuButtonAction);
-
// Update the menu when the active account changes.
// Shouldn't have to use lambdas here like this, but if I don't, the compiler throws a fit.
// Template hell sucks...
@@ -1061,35 +375,21 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow
updateNewsLabel();
}
-
- if(BuildConfig.UPDATER_ENABLED)
- {
+ if (BuildConfig.UPDATER_ENABLED) {
bool updatesAllowed = APPLICATION->updatesAreAllowed();
updatesAllowedChanged(updatesAllowed);
- // NOTE: calling the operator like that is an ugly hack to appease ancient gcc...
- connect(ui->actionCheckUpdate.operator->(), &QAction::triggered, this, &MainWindow::checkForUpdates);
+ connect(ui->actionCheckUpdate, &QAction::triggered, this, &MainWindow::checkForUpdates);
// set up the updater object.
- auto updater = APPLICATION->updateChecker();
- connect(updater.get(), &UpdateChecker::updateAvailable, this, &MainWindow::updateAvailable);
- connect(updater.get(), &UpdateChecker::noUpdateFound, this, &MainWindow::updateNotAvailable);
- // if automatic update checks are allowed, start one.
- if (APPLICATION->settings()->get("AutoUpdate").toBool() && updatesAllowed)
- {
- updater->checkForUpdate(APPLICATION->settings()->get("UpdateChannel").toString(), false);
- }
+ auto updater = APPLICATION->updater();
- if (APPLICATION->updateChecker()->getExternalUpdater())
- {
- connect(APPLICATION->updateChecker()->getExternalUpdater(),
- &ExternalUpdater::canCheckForUpdatesChanged,
- this,
- &MainWindow::updatesAllowedChanged);
+ if (updater) {
+ connect(updater.get(), &ExternalUpdater::canCheckForUpdatesChanged, this, &MainWindow::updatesAllowedChanged);
}
}
- connect(ui->actionUndoTrashInstance.operator->(), &QAction::triggered, this, &MainWindow::undoTrashInstance);
+ connect(ui->actionUndoTrashInstance, &QAction::triggered, this, &MainWindow::undoTrashInstance);
setSelectedInstanceById(APPLICATION->settings()->get("SelectedInstance").toString());
@@ -1117,10 +417,10 @@ void MainWindow::retranslateUi()
MinecraftAccountPtr defaultAccount = accounts->defaultAccount();
if(defaultAccount) {
auto profileLabel = profileInUseFilter(defaultAccount->profileName(), defaultAccount->isInUse());
- accountMenuButton->setText(profileLabel);
+ ui->actionAccountsButton->setText(profileLabel);
}
else {
- accountMenuButton->setText(tr("Accounts"));
+ ui->actionAccountsButton->setText(tr("Accounts"));
}
if (m_selectedInstance) {
@@ -1130,6 +430,20 @@ void MainWindow::retranslateUi()
}
ui->retranslateUi(this);
+
+ changeIconButton->setToolTip(ui->actionChangeInstIcon->toolTip());
+ renameButton->setToolTip(ui->actionRenameInstance->toolTip());
+
+ // replace the %1 with the launcher display name in some actions
+ if (helpMenuButton->toolTip().contains("%1"))
+ helpMenuButton->setToolTip(helpMenuButton->toolTip().arg(BuildConfig.LAUNCHER_DISPLAYNAME));
+
+ for (auto action : ui->helpMenu->actions()) {
+ if (action->text().contains("%1"))
+ action->setText(action->text().arg(BuildConfig.LAUNCHER_DISPLAYNAME));
+ if (action->toolTip().contains("%1"))
+ action->setToolTip(action->toolTip().arg(BuildConfig.LAUNCHER_DISPLAYNAME));
+ }
}
MainWindow::~MainWindow()
@@ -1169,13 +483,16 @@ void MainWindow::showInstanceContextMenu(const QPoint &pos)
bool onInstance = view->indexAt(pos).isValid();
if (onInstance)
{
- actions = ui->instanceToolBar->actions();
+ // reuse the file menu actions
+ actions = ui->fileMenu->actions();
- // replace the change icon widget with an actual action
- actions.replace(0, ui->actionChangeInstIcon);
+ // remove the add instance action, launcher settings action and close action
+ actions.removeFirst();
+ actions.removeLast();
+ actions.removeLast();
- // replace the rename widget with an actual action
- actions.replace(1, ui->actionRenameInstance);
+ actions.prepend(ui->actionChangeInstIcon);
+ actions.prepend(ui->actionRenameInstance);
// add header
actions.prepend(actionSep);
@@ -1231,8 +548,6 @@ void MainWindow::updateMainToolBar()
void MainWindow::updateToolsMenu()
{
- QToolButton *launchButton = dynamic_cast<QToolButton*>(ui->instanceToolBar->widgetForAction(ui->actionLaunchInstance));
-
bool currentInstanceRunning = m_selectedInstance && m_selectedInstance->isRunning();
ui->actionLaunchInstance->setDisabled(!m_selectedInstance || currentInstanceRunning);
@@ -1240,7 +555,6 @@ void MainWindow::updateToolsMenu()
ui->actionLaunchInstanceDemo->setDisabled(!m_selectedInstance || currentInstanceRunning);
QMenu *launchMenu = ui->actionLaunchInstance->menu();
- launchButton->setPopupMode(QToolButton::MenuButtonPopup);
if (launchMenu)
{
launchMenu->clear();
@@ -1249,7 +563,6 @@ void MainWindow::updateToolsMenu()
{
launchMenu = new QMenu(this);
}
-
QAction *normalLaunch = launchMenu->addAction(tr("Launch"));
normalLaunch->setShortcut(QKeySequence::Open);
QAction *normalLaunchOffline = launchMenu->addAction(tr("Launch Offline"));
@@ -1357,8 +670,7 @@ void MainWindow::updateThemeMenu()
void MainWindow::repopulateAccountsMenu()
{
- accountMenu->clear();
- ui->profileMenu->clear();
+ ui->accountsMenu->clear();
auto accounts = APPLICATION->accounts();
MinecraftAccountPtr defaultAccount = accounts->defaultAccount();
@@ -1367,23 +679,17 @@ void MainWindow::repopulateAccountsMenu()
if (defaultAccount)
{
// this can be called before accountMenuButton exists
- if (accountMenuButton)
+ if (ui->actionAccountsButton)
{
auto profileLabel = profileInUseFilter(defaultAccount->profileName(), defaultAccount->isInUse());
- accountMenuButton->setText(profileLabel);
+ ui->actionAccountsButton->setText(profileLabel);
}
}
if (accounts->count() <= 0)
{
- ui->all_actions.removeAll(&ui->actionNoAccountsAdded);
- ui->actionNoAccountsAdded = TranslatedAction(this);
- ui->actionNoAccountsAdded->setObjectName(QStringLiteral("actionNoAccountsAdded"));
- ui->actionNoAccountsAdded.setTextId(QT_TRANSLATE_NOOP("MainWindow", "No accounts added!"));
ui->actionNoAccountsAdded->setEnabled(false);
- accountMenu->addAction(ui->actionNoAccountsAdded);
- ui->profileMenu->addAction(ui->actionNoAccountsAdded);
- ui->all_actions.append(&ui->actionNoAccountsAdded);
+ ui->accountsMenu->addAction(ui->actionNoAccountsAdded);
}
else
{
@@ -1414,37 +720,22 @@ void MainWindow::repopulateAccountsMenu()
action->setShortcut(QKeySequence(tr("Ctrl+%1").arg(i + 1)));
}
- accountMenu->addAction(action);
- ui->profileMenu->addAction(action);
+ ui->accountsMenu->addAction(action);
connect(action, SIGNAL(triggered(bool)), SLOT(changeActiveAccount()));
}
}
- accountMenu->addSeparator();
- ui->profileMenu->addSeparator();
+ ui->accountsMenu->addSeparator();
- ui->all_actions.removeAll(&ui->actionNoDefaultAccount);
- ui->actionNoDefaultAccount = TranslatedAction(this);
- ui->actionNoDefaultAccount->setObjectName(QStringLiteral("actionNoDefaultAccount"));
- ui->actionNoDefaultAccount.setTextId(QT_TRANSLATE_NOOP("MainWindow", "No Default Account"));
- ui->actionNoDefaultAccount->setCheckable(true);
- ui->actionNoDefaultAccount->setIcon(APPLICATION->getThemedIcon("noaccount"));
ui->actionNoDefaultAccount->setData(-1);
- ui->actionNoDefaultAccount->setShortcut(QKeySequence(tr("Ctrl+0")));
- if (!defaultAccount) {
- ui->actionNoDefaultAccount->setChecked(true);
- }
+ ui->actionNoDefaultAccount->setChecked(!defaultAccount);
+
+ ui->accountsMenu->addAction(ui->actionNoDefaultAccount);
- accountMenu->addAction(ui->actionNoDefaultAccount);
- ui->profileMenu->addAction(ui->actionNoDefaultAccount);
connect(ui->actionNoDefaultAccount, SIGNAL(triggered(bool)), SLOT(changeActiveAccount()));
- ui->all_actions.append(&ui->actionNoDefaultAccount);
- ui->actionNoDefaultAccount.retranslate();
- accountMenu->addSeparator();
- ui->profileMenu->addSeparator();
- accountMenu->addAction(ui->actionManageAccounts);
- ui->profileMenu->addAction(ui->actionManageAccounts);
+ ui->accountsMenu->addSeparator();
+ ui->accountsMenu->addAction(ui->actionManageAccounts);
}
void MainWindow::updatesAllowedChanged(bool allowed)
@@ -1488,20 +779,20 @@ void MainWindow::defaultAccountChanged()
if (account && account->profileName() != "")
{
auto profileLabel = profileInUseFilter(account->profileName(), account->isInUse());
- accountMenuButton->setText(profileLabel);
+ ui->actionAccountsButton->setText(profileLabel);
auto face = account->getFace();
if(face.isNull()) {
- accountMenuButton->setIcon(APPLICATION->getThemedIcon("noaccount"));
+ ui->actionAccountsButton->setIcon(APPLICATION->getThemedIcon("noaccount"));
}
else {
- accountMenuButton->setIcon(face);
+ ui->actionAccountsButton->setIcon(face);
}
return;
}
// Set the icon to the "no account" icon.
- accountMenuButton->setIcon(APPLICATION->getThemedIcon("noaccount"));
- accountMenuButton->setText(tr("Accounts"));
+ ui->actionAccountsButton->setIcon(APPLICATION->getThemedIcon("noaccount"));
+ ui->actionAccountsButton->setText(tr("Accounts"));
}
bool MainWindow::eventFilter(QObject *obj, QEvent *ev)
@@ -1563,32 +854,6 @@ void MainWindow::updateNewsLabel()
}
}
-void MainWindow::updateAvailable(GoUpdate::Status status)
-{
- if(!APPLICATION->updatesAreAllowed())
- {
- updateNotAvailable();
- return;
- }
- UpdateDialog dlg(true, this);
- UpdateAction action = (UpdateAction)dlg.exec();
- switch (action)
- {
- case UPDATE_LATER:
- qDebug() << "Update will be installed later.";
- break;
- case UPDATE_NOW:
- downloadUpdates(status);
- break;
- }
-}
-
-void MainWindow::updateNotAvailable()
-{
- UpdateDialog dlg(false, this);
- dlg.exec();
-}
-
QList<int> stringToIntList(const QString &string)
{
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
@@ -1613,40 +878,6 @@ QString intListToString(const QList<int> &list)
return slist.join(',');
}
-void MainWindow::downloadUpdates(GoUpdate::Status status)
-{
- if(!APPLICATION->updatesAreAllowed())
- {
- return;
- }
- qDebug() << "Downloading updates.";
- ProgressDialog updateDlg(this);
- status.rootPath = APPLICATION->root();
-
- auto dlPath = FS::PathCombine(APPLICATION->root(), "update", "XXXXXX");
- if (!FS::ensureFilePathExists(dlPath))
- {
- CustomMessageBox::selectable(this, tr("Error"), tr("Couldn't create folder for update downloads:\n%1").arg(dlPath), QMessageBox::Warning)->show();
- }
- GoUpdate::DownloadTask updateTask(APPLICATION->network(), status, dlPath, &updateDlg);
- // If the task succeeds, install the updates.
- if (updateDlg.execWithTask(&updateTask))
- {
- /**
- * NOTE: This disables launching instances until the update either succeeds (and this process exits)
- * or the update fails (and the control leaves this scope).
- */
- APPLICATION->updateIsRunning(true);
- UpdateController update(this, APPLICATION->root(), updateTask.updateFilesDir(), updateTask.operations());
- update.installUpdates();
- APPLICATION->updateIsRunning(false);
- }
- else
- {
- CustomMessageBox::selectable(this, tr("Error"), updateTask.failReason(), QMessageBox::Warning)->show();
- }
-}
-
void MainWindow::onCatToggled(bool state)
{
setCatBackground(state);
@@ -1789,10 +1020,12 @@ void MainWindow::on_actionAddInstance_triggered()
addInstance();
}
-void MainWindow::droppedURLs(QList<QUrl> urls)
+void MainWindow::processURLs(QList<QUrl> urls)
{
// NOTE: This loop only processes one dropped file!
for (auto& url : urls) {
+ qDebug() << "Processing" << url;
+
// The isLocalFile() check below doesn't work as intended without an explicit scheme.
if (url.scheme().isEmpty())
url.setScheme("file");
@@ -1802,31 +1035,50 @@ void MainWindow::droppedURLs(QList<QUrl> urls)
break;
}
- auto localFileName = url.toLocalFile();
+ auto localFileName = QDir::toNativeSeparators(url.toLocalFile()) ;
QFileInfo localFileInfo(localFileName);
- bool isResourcePack = ResourcePackUtils::validate(localFileInfo);
- bool isTexturePack = TexturePackUtils::validate(localFileInfo);
+ auto type = ResourceUtils::identify(localFileInfo);
- if (!isResourcePack && !isTexturePack) { // probably instance/modpack
+ if (ResourceUtils::ValidResourceTypes.count(type) == 0) { // probably instance/modpack
addInstance(localFileName);
- break;
+ continue;
}
- ImportResourcePackDialog dlg(this);
+ ImportResourceDialog dlg(localFileName, type, this);
if (dlg.exec() != QDialog::Accepted)
- break;
+ continue;
- qDebug() << "Adding resource/texture pack" << localFileName << "to" << dlg.selectedInstanceKey;
+ qDebug() << "Adding resource" << localFileName << "to" << dlg.selectedInstanceKey;
auto inst = APPLICATION->instances()->getInstanceById(dlg.selectedInstanceKey);
auto minecraftInst = std::dynamic_pointer_cast<MinecraftInstance>(inst);
- if (isResourcePack)
- minecraftInst->resourcePackList()->installResource(localFileName);
- else if (isTexturePack)
- minecraftInst->texturePackList()->installResource(localFileName);
- break;
+
+ switch (type) {
+ case PackedResourceType::ResourcePack:
+ minecraftInst->resourcePackList()->installResource(localFileName);
+ break;
+ case PackedResourceType::TexturePack:
+ minecraftInst->texturePackList()->installResource(localFileName);
+ break;
+ case PackedResourceType::DataPack:
+ qWarning() << "Importing of Data Packs not supported at this time. Ignoring" << localFileName;
+ break;
+ case PackedResourceType::Mod:
+ minecraftInst->loaderModList()->installMod(localFileName);
+ break;
+ case PackedResourceType::ShaderPack:
+ minecraftInst->shaderPackList()->installResource(localFileName);
+ break;
+ case PackedResourceType::WorldSave:
+ minecraftInst->worldList()->installWorld(localFileInfo);
+ break;
+ case PackedResourceType::UNKNOWN:
+ default:
+ qDebug() << "Can't Identify" << localFileName << "Ignoring it.";
+ break;
+ }
}
}
@@ -1857,7 +1109,7 @@ void MainWindow::on_actionChangeInstIcon_triggered()
m_selectedInstance->setIconKey(dlg.selectedIconKey);
auto icon = APPLICATION->icons()->getIcon(dlg.selectedIconKey);
ui->actionChangeInstIcon->setIcon(icon);
- ui->changeIconButton->setIcon(icon);
+ changeIconButton->setIcon(icon);
}
}
@@ -1867,7 +1119,7 @@ void MainWindow::iconUpdated(QString icon)
{
auto icon = APPLICATION->icons()->getIcon(m_currentInstIcon);
ui->actionChangeInstIcon->setIcon(icon);
- ui->changeIconButton->setIcon(icon);
+ changeIconButton->setIcon(icon);
}
}
@@ -1876,7 +1128,7 @@ void MainWindow::updateInstanceToolIcon(QString new_icon)
m_currentInstIcon = new_icon;
auto icon = APPLICATION->icons()->getIcon(m_currentInstIcon);
ui->actionChangeInstIcon->setIcon(icon);
- ui->changeIconButton->setIcon(icon);
+ changeIconButton->setIcon(icon);
}
void MainWindow::setSelectedInstanceById(const QString &id)
@@ -1962,8 +1214,7 @@ void MainWindow::checkForUpdates()
{
if(BuildConfig.UPDATER_ENABLED)
{
- auto updater = APPLICATION->updateChecker();
- updater->checkForUpdate(APPLICATION->settings()->get("UpdateChannel").toString(), true);
+ APPLICATION->triggerUpdateCheck();
}
else
{
@@ -2056,6 +1307,10 @@ void MainWindow::newsButtonClicked()
news_dialog.exec();
}
+void MainWindow::onCatChanged(int) {
+ setCatBackground(APPLICATION->settings()->get("TheCat").toBool());
+}
+
void MainWindow::on_actionAbout_triggered()
{
AboutDialog dialog(this);
@@ -2120,6 +1375,7 @@ void MainWindow::closeEvent(QCloseEvent *event)
// Save the window state and geometry.
APPLICATION->settings()->set("MainWindowState", saveState().toBase64());
APPLICATION->settings()->set("MainWindowGeometry", saveGeometry().toBase64());
+ instanceToolbarSetting->set(ui->instanceToolBar->getVisibilityState());
event->accept();
emit isClosing();
}
@@ -2353,7 +1609,7 @@ void MainWindow::instanceChanged(const QModelIndex &current, const QModelIndex &
if (m_selectedInstance)
{
ui->instanceToolBar->setEnabled(true);
- ui->setInstanceActionsEnabled(true);
+ setInstanceActionsEnabled(true);
ui->actionLaunchInstance->setEnabled(m_selectedInstance->canLaunch());
ui->actionLaunchInstanceOffline->setEnabled(m_selectedInstance->canLaunch());
ui->actionLaunchInstanceDemo->setEnabled(m_selectedInstance->canLaunch());
@@ -2366,7 +1622,7 @@ void MainWindow::instanceChanged(const QModelIndex &current, const QModelIndex &
ui->actionKillInstance->setEnabled(m_selectedInstance->isRunning());
ui->actionExportInstance->setEnabled(m_selectedInstance->canExport());
- ui->renameButton->setText(m_selectedInstance->name());
+ renameButton->setText(m_selectedInstance->name());
m_statusLeft->setText(m_selectedInstance->getStatusbarDescription());
updateStatusCenter();
updateInstanceToolIcon(m_selectedInstance->iconKey());
@@ -2380,7 +1636,7 @@ void MainWindow::instanceChanged(const QModelIndex &current, const QModelIndex &
else
{
ui->instanceToolBar->setEnabled(false);
- ui->setInstanceActionsEnabled(false);
+ setInstanceActionsEnabled(false);
ui->actionLaunchInstance->setEnabled(false);
ui->actionLaunchInstanceOffline->setEnabled(false);
ui->actionLaunchInstanceDemo->setEnabled(false);
@@ -2413,9 +1669,9 @@ void MainWindow::selectionBad()
statusBar()->clearMessage();
ui->instanceToolBar->setEnabled(false);
- ui->setInstanceActionsEnabled(false);
+ setInstanceActionsEnabled(false);
updateToolsMenu();
- ui->renameButton->setText(tr("Rename Instance"));
+ renameButton->setText(tr("Rename Instance"));
updateInstanceToolIcon("grass");
// ...and then see if we can enable the previously selected instance
@@ -2470,6 +1726,18 @@ void MainWindow::updateStatusCenter()
m_statusCenter->setText(tr("Total playtime: %1").arg(Time::prettifyDuration(timePlayed)));
}
}
+// "Instance actions" are actions that require an instance to be selected (i.e. "new instance" is not here)
+// Actions that also require other conditions (e.g. a running instance) won't be changed.
+void MainWindow::setInstanceActionsEnabled(bool enabled)
+{
+ ui->actionEditInstance->setEnabled(enabled);
+ ui->actionChangeInstGroup->setEnabled(enabled);
+ ui->actionViewSelectedInstFolder->setEnabled(enabled);
+ ui->actionExportInstance->setEnabled(enabled);
+ ui->actionDeleteInstance->setEnabled(enabled);
+ ui->actionCopyInstance->setEnabled(enabled);
+ ui->actionCreateInstanceShortcut->setEnabled(enabled);
+}
void MainWindow::refreshCurrentInstance(bool running)
{
diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h
index f96f641d..3a42c34e 100644
--- a/launcher/ui/MainWindow.h
+++ b/launcher/ui/MainWindow.h
@@ -48,7 +48,6 @@
#include "BaseInstance.h"
#include "minecraft/auth/MinecraftAccount.h"
#include "net/NetJob.h"
-#include "updater/GoUpdate.h"
class LaunchController;
class NewsChecker;
@@ -61,13 +60,16 @@ class BaseProfilerFactory;
class InstanceView;
class KonamiCode;
class InstanceTask;
+class LabeledToolButton;
+namespace Ui
+{
+class MainWindow;
+}
class MainWindow : public QMainWindow
{
Q_OBJECT
- class Ui;
-
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
@@ -80,7 +82,7 @@ public:
void updatesAllowedChanged(bool allowed);
- void droppedURLs(QList<QUrl> urls);
+ void processURLs(QList<QUrl> urls);
signals:
void isClosing();
@@ -90,6 +92,8 @@ protected:
private slots:
void onCatToggled(bool);
+ void onCatChanged(int);
+
void on_actionAbout_triggered();
void on_actionAddInstance_triggered();
@@ -105,10 +109,6 @@ private slots:
void on_actionChangeInstGroup_triggered();
void on_actionChangeInstIcon_triggered();
- void on_changeIconButton_clicked(bool)
- {
- on_actionChangeInstIcon_triggered();
- }
void on_actionViewInstanceFolder_triggered();
@@ -154,10 +154,6 @@ private slots:
void on_actionExportInstance_triggered();
void on_actionRenameInstance_triggered();
- void on_renameButton_clicked(bool)
- {
- on_actionRenameInstance_triggered();
- }
void on_actionEditInstance_triggered();
@@ -190,10 +186,6 @@ private slots:
void startTask(Task *task);
- void updateAvailable(GoUpdate::Status status);
-
- void updateNotAvailable();
-
void defaultAccountChanged();
void changeActiveAccount();
@@ -202,10 +194,6 @@ private slots:
void updateNewsLabel();
- /*!
- * Runs the DownloadTask and installs updates.
- */
- void downloadUpdates(GoUpdate::Status status);
void konamiTriggered();
@@ -228,24 +216,27 @@ private:
void updateInstanceToolIcon(QString new_icon);
void setSelectedInstanceById(const QString &id);
void updateStatusCenter();
+ void setInstanceActionsEnabled(bool enabled);
void runModalTask(Task *task);
void instanceFromInstanceTask(InstanceTask *task);
void finalizeInstance(InstancePtr inst);
private:
- std::unique_ptr<Ui> ui;
-
+ Ui::MainWindow *ui;
// these are managed by Qt's memory management model!
InstanceView *view = nullptr;
InstanceProxyModel *proxymodel = nullptr;
QToolButton *newsLabel = nullptr;
QLabel *m_statusLeft = nullptr;
QLabel *m_statusCenter = nullptr;
- QMenu *accountMenu = nullptr;
- QToolButton *accountMenuButton = nullptr;
+ LabeledToolButton *changeIconButton = nullptr;
+ LabeledToolButton *renameButton = nullptr;
+ QToolButton *helpMenuButton = nullptr;
KonamiCode * secretEventFilter = nullptr;
+ std::shared_ptr<Setting> instanceToolbarSetting = nullptr;
+
unique_qobject_ptr<NewsChecker> m_newsChecker;
InstancePtr m_selectedInstance;
@@ -254,4 +245,3 @@ private:
// managed by the application object
Task *m_versionLoadTask = nullptr;
};
-
diff --git a/launcher/ui/MainWindow.ui b/launcher/ui/MainWindow.ui
new file mode 100644
index 00000000..2b6a10b1
--- /dev/null
+++ b/launcher/ui/MainWindow.ui
@@ -0,0 +1,690 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>MainWindow</class>
+ <widget class="QMainWindow" name="MainWindow">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>800</width>
+ <height>600</height>
+ </rect>
+ </property>
+ <widget class="QWidget" name="centralWidget">
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <property name="spacing">
+ <number>0</number>
+ </property>
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ </layout>
+ </widget>
+ <widget class="QStatusBar" name="statusBar"/>
+ <widget class="QToolBar" name="mainToolBar">
+ <property name="windowTitle">
+ <string>Main Toolbar</string>
+ </property>
+ <property name="allowedAreas">
+ <set>Qt::BottomToolBarArea|Qt::TopToolBarArea</set>
+ </property>
+ <property name="toolButtonStyle">
+ <enum>Qt::ToolButtonTextBesideIcon</enum>
+ </property>
+ <property name="floatable">
+ <bool>false</bool>
+ </property>
+ <attribute name="toolBarArea">
+ <enum>TopToolBarArea</enum>
+ </attribute>
+ <attribute name="toolBarBreak">
+ <bool>false</bool>
+ </attribute>
+ <addaction name="actionAddInstance"/>
+ <addaction name="separator"/>
+ <addaction name="actionFoldersButton"/>
+ <addaction name="actionSettings"/>
+ <addaction name="actionHelpButton"/>
+ <addaction name="actionCheckUpdate"/>
+ <addaction name="separator"/>
+ <addaction name="actionCAT"/>
+ <addaction name="actionAccountsButton"/>
+ </widget>
+ <widget class="QToolBar" name="newsToolBar">
+ <property name="windowTitle">
+ <string>News Toolbar</string>
+ </property>
+ <property name="allowedAreas">
+ <set>Qt::BottomToolBarArea|Qt::TopToolBarArea</set>
+ </property>
+ <property name="iconSize">
+ <size>
+ <width>16</width>
+ <height>16</height>
+ </size>
+ </property>
+ <property name="toolButtonStyle">
+ <enum>Qt::ToolButtonTextBesideIcon</enum>
+ </property>
+ <property name="floatable">
+ <bool>false</bool>
+ </property>
+ <attribute name="toolBarArea">
+ <enum>BottomToolBarArea</enum>
+ </attribute>
+ <attribute name="toolBarBreak">
+ <bool>false</bool>
+ </attribute>
+ <addaction name="actionMoreNews"/>
+ </widget>
+ <widget class="WideBar" name="instanceToolBar">
+ <property name="windowTitle">
+ <string>Instance Toolbar</string>
+ </property>
+ <property name="allowedAreas">
+ <set>Qt::LeftToolBarArea|Qt::RightToolBarArea</set>
+ </property>
+ <property name="iconSize">
+ <size>
+ <width>16</width>
+ <height>16</height>
+ </size>
+ </property>
+ <property name="toolButtonStyle">
+ <enum>Qt::ToolButtonTextBesideIcon</enum>
+ </property>
+ <property name="floatable">
+ <bool>false</bool>
+ </property>
+ <property name="useDefaultAction" stdset="0">
+ <bool>true</bool>
+ </property>
+ <attribute name="toolBarArea">
+ <enum>RightToolBarArea</enum>
+ </attribute>
+ <attribute name="toolBarBreak">
+ <bool>false</bool>
+ </attribute>
+ <addaction name="actionLaunchInstance"/>
+ <addaction name="actionKillInstance"/>
+ <addaction name="separator"/>
+ <addaction name="actionEditInstance"/>
+ <addaction name="actionChangeInstGroup"/>
+ <addaction name="actionViewSelectedInstFolder"/>
+ <addaction name="actionExportInstance"/>
+ <addaction name="actionCopyInstance"/>
+ <addaction name="actionDeleteInstance"/>
+ <addaction name="actionCreateInstanceShortcut"/>
+ </widget>
+ <widget class="QMenuBar" name="menuBar">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>800</width>
+ <height>20</height>
+ </rect>
+ </property>
+ <widget class="QMenu" name="fileMenu">
+ <property name="title">
+ <string>&amp;File</string>
+ </property>
+ <property name="toolTipsVisible">
+ <bool>true</bool>
+ </property>
+ <addaction name="actionAddInstance"/>
+ <addaction name="separator"/>
+ <addaction name="actionLaunchInstance"/>
+ <addaction name="actionKillInstance"/>
+ <addaction name="separator"/>
+ <addaction name="actionEditInstance"/>
+ <addaction name="actionChangeInstGroup"/>
+ <addaction name="actionViewSelectedInstFolder"/>
+ <addaction name="actionExportInstance"/>
+ <addaction name="actionCopyInstance"/>
+ <addaction name="actionDeleteInstance"/>
+ <addaction name="actionCreateInstanceShortcut"/>
+ <addaction name="separator"/>
+ <addaction name="actionSettings"/>
+ <addaction name="actionCloseWindow"/>
+ </widget>
+ <widget class="QMenu" name="editMenu">
+ <property name="title">
+ <string>&amp;Edit</string>
+ </property>
+ <property name="toolTipsVisible">
+ <bool>true</bool>
+ </property>
+ <addaction name="actionUndoTrashInstance"/>
+ </widget>
+ <widget class="QMenu" name="viewMenu">
+ <property name="title">
+ <string>&amp;View</string>
+ </property>
+ <property name="toolTipsVisible">
+ <bool>true</bool>
+ </property>
+ <addaction name="actionChangeTheme"/>
+ <addaction name="separator"/>
+ <addaction name="actionCAT"/>
+ <addaction name="actionLockToolbars"/>
+ <addaction name="separator"/>
+ </widget>
+ <widget class="QMenu" name="foldersMenu">
+ <property name="title">
+ <string>F&amp;olders</string>
+ </property>
+ <property name="toolTipsVisible">
+ <bool>true</bool>
+ </property>
+ <addaction name="actionViewInstanceFolder"/>
+ <addaction name="actionViewCentralModsFolder"/>
+ </widget>
+ <widget class="QMenu" name="accountsMenu">
+ <property name="title">
+ <string>&amp;Accounts</string>
+ </property>
+ </widget>
+ <widget class="QMenu" name="helpMenu">
+ <property name="title">
+ <string>&amp;Help</string>
+ </property>
+ <property name="toolTipsVisible">
+ <bool>true</bool>
+ </property>
+ <addaction name="actionClearMetadata"/>
+ <addaction name="actionReportBug"/>
+ <addaction name="actionAddToPATH"/>
+ <addaction name="separator"/>
+ <addaction name="actionMATRIX"/>
+ <addaction name="actionDISCORD"/>
+ <addaction name="actionREDDIT"/>
+ <addaction name="separator"/>
+ <addaction name="actionMoreNews"/>
+ <addaction name="actionCheckUpdate"/>
+ <addaction name="actionOpenWiki"/>
+ <addaction name="actionAbout"/>
+ </widget>
+ <addaction name="fileMenu"/>
+ <addaction name="editMenu"/>
+ <addaction name="viewMenu"/>
+ <addaction name="foldersMenu"/>
+ <addaction name="accountsMenu"/>
+ <addaction name="helpMenu"/>
+ </widget>
+ <action name="actionMoreNews">
+ <property name="icon">
+ <iconset theme="news">
+ <normaloff>.</normaloff>.</iconset>
+ </property>
+ <property name="text">
+ <string>More news...</string>
+ </property>
+ <property name="toolTip">
+ <string>Open the development blog to read more news about %1.</string>
+ </property>
+ </action>
+ <action name="actionCAT">
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="icon">
+ <iconset theme="cat">
+ <normaloff>.</normaloff>.</iconset>
+ </property>
+ <property name="text">
+ <string>&amp;Meow</string>
+ </property>
+ <property name="toolTip">
+ <string>It's a fluffy kitty :3</string>
+ </property>
+ </action>
+ <action name="actionLockToolbars">
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="text">
+ <string>Lock Toolbars</string>
+ </property>
+ </action>
+ <action name="actionUndoTrashInstance">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="text">
+ <string>&amp;Undo Last Instance Deletion</string>
+ </property>
+ </action>
+ <action name="actionAddInstance">
+ <property name="icon">
+ <iconset theme="new">
+ <normaloff>.</normaloff>.</iconset>
+ </property>
+ <property name="text">
+ <string>Add Instanc&amp;e...</string>
+ </property>
+ <property name="toolTip">
+ <string>Add a new instance.</string>
+ </property>
+ </action>
+ <action name="actionCheckUpdate">
+ <property name="icon">
+ <iconset theme="checkupdate">
+ <normaloff>.</normaloff>.</iconset>
+ </property>
+ <property name="text">
+ <string>&amp;Update...</string>
+ </property>
+ <property name="toolTip">
+ <string>Check for new updates for %1.</string>
+ </property>
+ <property name="menuRole">
+ <enum>QAction::ApplicationSpecificRole</enum>
+ </property>
+ </action>
+ <action name="actionSettings">
+ <property name="icon">
+ <iconset theme="settings">
+ <normaloff>.</normaloff>.</iconset>
+ </property>
+ <property name="text">
+ <string>Setti&amp;ngs...</string>
+ </property>
+ <property name="toolTip">
+ <string>Change settings.</string>
+ </property>
+ <property name="menuRole">
+ <enum>QAction::PreferencesRole</enum>
+ </property>
+ </action>
+ <action name="actionManageAccounts">
+ <property name="icon">
+ <iconset theme="accounts">
+ <normaloff>.</normaloff>.</iconset>
+ </property>
+ <property name="text">
+ <string>&amp;Manage Accounts...</string>
+ </property>
+ </action>
+ <action name="actionLaunchInstance">
+ <property name="icon">
+ <iconset theme="launch">
+ <normaloff>.</normaloff>.</iconset>
+ </property>
+ <property name="text">
+ <string>&amp;Launch</string>
+ </property>
+ <property name="toolTip">
+ <string>Launch the selected instance.</string>
+ </property>
+ </action>
+ <action name="actionKillInstance">
+ <property name="icon">
+ <iconset theme="status-bad">
+ <normaloff>.</normaloff>.</iconset>
+ </property>
+ <property name="text">
+ <string>&amp;Kill</string>
+ </property>
+ <property name="toolTip">
+ <string>Kill the running instance.</string>
+ </property>
+ <property name="shortcut">
+ <string>Ctrl+K</string>
+ </property>
+ </action>
+ <action name="actionRenameInstance">
+ <property name="icon">
+ <iconset theme="rename">
+ <normaloff>.</normaloff>.</iconset>
+ </property>
+ <property name="text">
+ <string>Rename</string>
+ </property>
+ <property name="toolTip">
+ <string>Rename the selected instance.</string>
+ </property>
+ </action>
+ <action name="actionChangeInstGroup">
+ <property name="icon">
+ <iconset theme="tag">
+ <normaloff>.</normaloff>.</iconset>
+ </property>
+ <property name="text">
+ <string>&amp;Change Group...</string>
+ </property>
+ <property name="toolTip">
+ <string>Change the selected instance's group.</string>
+ </property>
+ <property name="shortcut">
+ <string>Ctrl+G</string>
+ </property>
+ </action>
+ <action name="actionChangeInstIcon">
+ <property name="text">
+ <string>Change Icon</string>
+ </property>
+ <property name="toolTip">
+ <string>Change the selected instance's icon.</string>
+ </property>
+ </action>
+ <action name="actionEditInstance">
+ <property name="icon">
+ <iconset theme="settings">
+ <normaloff>.</normaloff>.</iconset>
+ </property>
+ <property name="text">
+ <string>&amp;Edit...</string>
+ </property>
+ <property name="toolTip">
+ <string>Change the instance settings, mods and versions.</string>
+ </property>
+ <property name="shortcut">
+ <string>Ctrl+I</string>
+ </property>
+ </action>
+ <action name="actionViewSelectedInstFolder">
+ <property name="icon">
+ <iconset theme="viewfolder">
+ <normaloff>.</normaloff>.</iconset>
+ </property>
+ <property name="text">
+ <string>&amp;Folder</string>
+ </property>
+ <property name="toolTip">
+ <string>Open the selected instance's root folder in a file browser.</string>
+ </property>
+ </action>
+ <action name="actionDeleteInstance">
+ <property name="icon">
+ <iconset theme="delete">
+ <normaloff>.</normaloff>.</iconset>
+ </property>
+ <property name="text">
+ <string>Dele&amp;te</string>
+ </property>
+ <property name="toolTip">
+ <string>Delete the selected instance.</string>
+ </property>
+ <property name="autoRepeat">
+ <bool>false</bool>
+ </property>
+ </action>
+ <action name="actionCopyInstance">
+ <property name="icon">
+ <iconset theme="copy">
+ <normaloff>.</normaloff>.</iconset>
+ </property>
+ <property name="text">
+ <string>Cop&amp;y...</string>
+ </property>
+ <property name="toolTip">
+ <string>Copy the selected instance.</string>
+ </property>
+ <property name="shortcut">
+ <string>Ctrl+D</string>
+ </property>
+ </action>
+ <action name="actionLaunchInstanceOffline">
+ <property name="text">
+ <string>Launch &amp;Offline</string>
+ </property>
+ <property name="toolTip">
+ <string>Launch the selected instance in offline mode.</string>
+ </property>
+ </action>
+ <action name="actionLaunchInstanceDemo">
+ <property name="text">
+ <string>Launch &amp;Demo</string>
+ </property>
+ <property name="toolTip">
+ <string>Launch the selected instance in demo mode.</string>
+ </property>
+ </action>
+ <action name="actionExportInstance">
+ <property name="icon">
+ <iconset theme="export">
+ <normaloff>.</normaloff>.</iconset>
+ </property>
+ <property name="text">
+ <string>E&amp;xport...</string>
+ </property>
+ <property name="toolTip">
+ <string>Export the selected instance as a zip file.</string>
+ </property>
+ <property name="shortcut">
+ <string>Ctrl+E</string>
+ </property>
+ </action>
+ <action name="actionCreateInstanceShortcut">
+ <property name="icon">
+ <iconset theme="shortcut">
+ <normaloff>.</normaloff>.</iconset>
+ </property>
+ <property name="text">
+ <string>Create Shortcut</string>
+ </property>
+ <property name="toolTip">
+ <string>Creates a shortcut on your desktop to launch the selected instance.</string>
+ </property>
+ </action>
+ <action name="actionNoAccountsAdded">
+ <property name="icon">
+ <iconset theme="noaccount">
+ <normaloff>.</normaloff>.</iconset>
+ </property>
+ <property name="text">
+ <string>No accounts added!</string>
+ </property>
+ </action>
+ <action name="actionNoDefaultAccount">
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="icon">
+ <iconset theme="noaccount">
+ <normaloff>.</normaloff>.</iconset>
+ </property>
+ <property name="text">
+ <string>No Default Account</string>
+ </property>
+ <property name="shortcut">
+ <string>Ctrl+0</string>
+ </property>
+ </action>
+ <action name="actionCloseWindow">
+ <property name="icon">
+ <iconset theme="status-bad">
+ <normaloff>.</normaloff>.</iconset>
+ </property>
+ <property name="text">
+ <string>Close &amp;Window</string>
+ </property>
+ <property name="toolTip">
+ <string>Close the current window</string>
+ </property>
+ <property name="menuRole">
+ <enum>QAction::QuitRole</enum>
+ </property>
+ </action>
+ <action name="actionViewInstanceFolder">
+ <property name="icon">
+ <iconset theme="viewfolder">
+ <normaloff>.</normaloff>.</iconset>
+ </property>
+ <property name="text">
+ <string>&amp;View Instance Folder</string>
+ </property>
+ <property name="toolTip">
+ <string>Open the instance folder in a file browser.</string>
+ </property>
+ </action>
+ <action name="actionViewCentralModsFolder">
+ <property name="icon">
+ <iconset theme="centralmods">
+ <normaloff>.</normaloff>.</iconset>
+ </property>
+ <property name="text">
+ <string>View &amp;Central Mods Folder</string>
+ </property>
+ <property name="toolTip">
+ <string>Open the central mods folder in a file browser.</string>
+ </property>
+ </action>
+ <action name="actionChangeTheme">
+ <property name="text">
+ <string>Themes</string>
+ </property>
+ </action>
+ <action name="actionReportBug">
+ <property name="icon">
+ <iconset theme="bug">
+ <normaloff>.</normaloff>.</iconset>
+ </property>
+ <property name="text">
+ <string>Report a &amp;Bug...</string>
+ </property>
+ <property name="toolTip">
+ <string>Open the bug tracker to report a bug with %1.</string>
+ </property>
+ </action>
+ <action name="actionDISCORD">
+ <property name="icon">
+ <iconset theme="discord">
+ <normaloff>.</normaloff>.</iconset>
+ </property>
+ <property name="text">
+ <string>&amp;Discord Guild</string>
+ </property>
+ <property name="toolTip">
+ <string>Open %1 Discord guild.</string>
+ </property>
+ </action>
+ <action name="actionMATRIX">
+ <property name="icon">
+ <iconset theme="matrix">
+ <normaloff>.</normaloff>.</iconset>
+ </property>
+ <property name="text">
+ <string>&amp;Matrix Space</string>
+ </property>
+ <property name="toolTip">
+ <string>Open %1 Matrix space.</string>
+ </property>
+ </action>
+ <action name="actionREDDIT">
+ <property name="icon">
+ <iconset theme="reddit-alien">
+ <normaloff>.</normaloff>.</iconset>
+ </property>
+ <property name="text">
+ <string>Sub&amp;reddit</string>
+ </property>
+ <property name="toolTip">
+ <string>Open %1 subreddit.</string>
+ </property>
+ </action>
+ <action name="actionAbout">
+ <property name="icon">
+ <iconset theme="about">
+ <normaloff>.</normaloff>.</iconset>
+ </property>
+ <property name="text">
+ <string>&amp;About %1</string>
+ </property>
+ <property name="toolTip">
+ <string>View information about %1.</string>
+ </property>
+ <property name="menuRole">
+ <enum>QAction::AboutRole</enum>
+ </property>
+ </action>
+ <action name="actionClearMetadata">
+ <property name="icon">
+ <iconset theme="refresh">
+ <normaloff>.</normaloff>.</iconset>
+ </property>
+ <property name="text">
+ <string>&amp;Clear Metadata Cache</string>
+ </property>
+ <property name="toolTip">
+ <string>Clear cached metadata</string>
+ </property>
+ </action>
+ <action name="actionAddToPATH">
+ <property name="icon">
+ <iconset theme="custom-commands">
+ <normaloff>.</normaloff>.</iconset>
+ </property>
+ <property name="text">
+ <string>Install to &amp;PATH</string>
+ </property>
+ <property name="toolTip">
+ <string>Install a %1 symlink to /usr/local/bin</string>
+ </property>
+ </action>
+ <action name="actionFoldersButton">
+ <property name="icon">
+ <iconset theme="viewfolder">
+ <normaloff>.</normaloff>.</iconset>
+ </property>
+ <property name="text">
+ <string>Folders</string>
+ </property>
+ <property name="toolTip">
+ <string>Open one of the folders shared between instances.</string>
+ </property>
+ </action>
+ <action name="actionHelpButton">
+ <property name="icon">
+ <iconset theme="help">
+ <normaloff>.</normaloff>.</iconset>
+ </property>
+ <property name="text">
+ <string>Help</string>
+ </property>
+ <property name="toolTip">
+ <string>Get help with %1 or Minecraft.</string>
+ </property>
+ </action>
+ <action name="actionAccountsButton">
+ <property name="icon">
+ <iconset theme="noaccount">
+ <normaloff>.</normaloff>.</iconset>
+ </property>
+ <property name="text">
+ <string>Accounts</string>
+ </property>
+ </action>
+ <action name="actionOpenWiki">
+ <property name="icon">
+ <iconset theme="help">
+ <normaloff>.</normaloff>.</iconset>
+ </property>
+ <property name="text">
+ <string>%1 &amp;Help</string>
+ </property>
+ <property name="toolTip">
+ <string>Open the %1 wiki</string>
+ </property>
+ </action>
+ </widget>
+ <customwidgets>
+ <customwidget>
+ <class>WideBar</class>
+ <extends>QToolBar</extends>
+ <header>ui/widgets/WideBar.h</header>
+ </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/ui/dialogs/BlockedModsDialog.cpp b/launcher/ui/dialogs/BlockedModsDialog.cpp
index 8b49bd1a..eb427953 100644
--- a/launcher/ui/dialogs/BlockedModsDialog.cpp
+++ b/launcher/ui/dialogs/BlockedModsDialog.cpp
@@ -32,10 +32,14 @@
#include <QDebug>
#include <QDesktopServices>
#include <QDialogButtonBox>
+#include <QDir>
+#include <QDirIterator>
#include <QDragEnterEvent>
#include <QFileDialog>
#include <QFileInfo>
+#include <QMimeData>
#include <QPushButton>
+#include <QMimeData>
#include <QStandardPaths>
BlockedModsDialog::BlockedModsDialog(QWidget* parent, const QString& title, const QString& text, QList<BlockedMod>& mods)
@@ -88,7 +92,7 @@ void BlockedModsDialog::dropEvent(QDropEvent* e)
if (url.scheme().isEmpty()) { // ensure isLocalFile() works correctly
url.setScheme("file");
}
-
+
if (!url.isLocalFile()) { // can't drop external files here.
continue;
}
@@ -230,7 +234,7 @@ void BlockedModsDialog::addHashTask(QString path)
/// @param path the path to the local file being hashed
void BlockedModsDialog::buildHashTask(QString path)
{
- auto hash_task = Hashing::createBlockedModHasher(path, ModPlatform::Provider::FLAME, "sha1");
+ auto hash_task = Hashing::createBlockedModHasher(path, ModPlatform::ResourceProvider::FLAME, "sha1");
qDebug() << "[Blocked Mods Dialog] Creating Hash task for path: " << path;
diff --git a/launcher/ui/dialogs/ChooseProviderDialog.cpp b/launcher/ui/dialogs/ChooseProviderDialog.cpp
index 89935d9a..83748e1e 100644
--- a/launcher/ui/dialogs/ChooseProviderDialog.cpp
+++ b/launcher/ui/dialogs/ChooseProviderDialog.cpp
@@ -67,9 +67,9 @@ void ChooseProviderDialog::confirmAll()
accept();
}
-auto ChooseProviderDialog::getSelectedProvider() const -> ModPlatform::Provider
+auto ChooseProviderDialog::getSelectedProvider() const -> ModPlatform::ResourceProvider
{
- return ModPlatform::Provider(m_providers.checkedId());
+ return ModPlatform::ResourceProvider(m_providers.checkedId());
}
void ChooseProviderDialog::addProviders()
@@ -77,7 +77,7 @@ void ChooseProviderDialog::addProviders()
int btn_index = 0;
QRadioButton* btn;
- for (auto& provider : { ModPlatform::Provider::MODRINTH, ModPlatform::Provider::FLAME }) {
+ for (auto& provider : { ModPlatform::ResourceProvider::MODRINTH, ModPlatform::ResourceProvider::FLAME }) {
btn = new QRadioButton(ProviderCaps.readableName(provider), this);
m_providers.addButton(btn, btn_index++);
ui->providersLayout->addWidget(btn);
diff --git a/launcher/ui/dialogs/ChooseProviderDialog.h b/launcher/ui/dialogs/ChooseProviderDialog.h
index 4a3b9f29..be9735b5 100644
--- a/launcher/ui/dialogs/ChooseProviderDialog.h
+++ b/launcher/ui/dialogs/ChooseProviderDialog.h
@@ -8,7 +8,7 @@ class ChooseProviderDialog;
}
namespace ModPlatform {
-enum class Provider;
+enum class ResourceProvider;
}
class Mod;
@@ -24,7 +24,7 @@ class ChooseProviderDialog : public QDialog {
bool try_others = false;
- ModPlatform::Provider chosen;
+ ModPlatform::ResourceProvider chosen;
};
public:
@@ -45,7 +45,7 @@ class ChooseProviderDialog : public QDialog {
void addProviders();
void disableInput();
- auto getSelectedProvider() const -> ModPlatform::Provider;
+ auto getSelectedProvider() const -> ModPlatform::ResourceProvider;
private:
Ui::ChooseProviderDialog* ui;
diff --git a/launcher/ui/dialogs/ExportInstanceDialog.cpp b/launcher/ui/dialogs/ExportInstanceDialog.cpp
index 88552b23..f13e36e8 100644
--- a/launcher/ui/dialogs/ExportInstanceDialog.cpp
+++ b/launcher/ui/dialogs/ExportInstanceDialog.cpp
@@ -44,6 +44,7 @@
#include <QSortFilterProxyModel>
#include <QDebug>
#include <QSaveFile>
+#include <QStack>
#include "StringUtils.h"
#include "SeparatorPrefixTree.h"
#include "Application.h"
diff --git a/launcher/ui/dialogs/ImportResourcePackDialog.cpp b/launcher/ui/dialogs/ImportResourceDialog.cpp
index e8902656..84b69273 100644
--- a/launcher/ui/dialogs/ImportResourcePackDialog.cpp
+++ b/launcher/ui/dialogs/ImportResourceDialog.cpp
@@ -1,5 +1,5 @@
-#include "ImportResourcePackDialog.h"
-#include "ui_ImportResourcePackDialog.h"
+#include "ImportResourceDialog.h"
+#include "ui_ImportResourceDialog.h"
#include <QFileDialog>
#include <QPushButton>
@@ -8,10 +8,11 @@
#include "InstanceList.h"
#include <InstanceList.h>
-#include "ui/instanceview/InstanceProxyModel.h"
#include "ui/instanceview/InstanceDelegate.h"
+#include "ui/instanceview/InstanceProxyModel.h"
-ImportResourcePackDialog::ImportResourcePackDialog(QWidget* parent) : QDialog(parent), ui(new Ui::ImportResourcePackDialog)
+ImportResourceDialog::ImportResourceDialog(QString file_path, PackedResourceType type, QWidget* parent)
+ : QDialog(parent), ui(new Ui::ImportResourceDialog), m_resource_type(type), m_file_path(file_path)
{
ui->setupUi(this);
setWindowModality(Qt::WindowModal);
@@ -40,15 +41,19 @@ ImportResourcePackDialog::ImportResourcePackDialog(QWidget* parent) : QDialog(pa
connect(contentsWidget, SIGNAL(doubleClicked(QModelIndex)), SLOT(activated(QModelIndex)));
connect(contentsWidget->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)),
SLOT(selectionChanged(QItemSelection, QItemSelection)));
+
+ ui->label->setText(
+ tr("Choose the instance you would like to import this %1 to.").arg(ResourceUtils::getPackedTypeName(m_resource_type)));
+ ui->label_file_path->setText(tr("File: %1").arg(m_file_path));
}
-void ImportResourcePackDialog::activated(QModelIndex index)
+void ImportResourceDialog::activated(QModelIndex index)
{
selectedInstanceKey = index.data(InstanceList::InstanceIDRole).toString();
accept();
}
-void ImportResourcePackDialog::selectionChanged(QItemSelection selected, QItemSelection deselected)
+void ImportResourceDialog::selectionChanged(QItemSelection selected, QItemSelection deselected)
{
if (selected.empty())
return;
@@ -59,7 +64,7 @@ void ImportResourcePackDialog::selectionChanged(QItemSelection selected, QItemSe
}
}
-ImportResourcePackDialog::~ImportResourcePackDialog()
+ImportResourceDialog::~ImportResourceDialog()
{
delete ui;
}
diff --git a/launcher/ui/dialogs/ImportResourceDialog.h b/launcher/ui/dialogs/ImportResourceDialog.h
new file mode 100644
index 00000000..5f2f7a92
--- /dev/null
+++ b/launcher/ui/dialogs/ImportResourceDialog.h
@@ -0,0 +1,30 @@
+#pragma once
+
+#include <QDialog>
+#include <QItemSelection>
+
+#include "minecraft/mod/tasks/LocalResourceParse.h"
+#include "ui/instanceview/InstanceProxyModel.h"
+
+namespace Ui {
+class ImportResourceDialog;
+}
+
+class ImportResourceDialog : public QDialog {
+ Q_OBJECT
+
+ public:
+ explicit ImportResourceDialog(QString file_path, PackedResourceType type, QWidget* parent = nullptr);
+ ~ImportResourceDialog() override;
+ QString selectedInstanceKey;
+
+ private:
+ Ui::ImportResourceDialog* ui;
+ PackedResourceType m_resource_type;
+ QString m_file_path;
+ InstanceProxyModel* proxyModel;
+
+ private slots:
+ void selectionChanged(QItemSelection, QItemSelection);
+ void activated(QModelIndex);
+};
diff --git a/launcher/ui/dialogs/ImportResourcePackDialog.ui b/launcher/ui/dialogs/ImportResourceDialog.ui
index 20cb9177..cc3f4ec1 100644
--- a/launcher/ui/dialogs/ImportResourcePackDialog.ui
+++ b/launcher/ui/dialogs/ImportResourceDialog.ui
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
- <class>ImportResourcePackDialog</class>
- <widget class="QDialog" name="ImportResourcePackDialog">
+ <class>ImportResourceDialog</class>
+ <widget class="QDialog" name="ImportResourceDialog">
<property name="geometry">
<rect>
<x>0</x>
@@ -11,7 +11,7 @@
</rect>
</property>
<property name="windowTitle">
- <string>Choose instance to import</string>
+ <string>Choose instance to import to</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
@@ -22,6 +22,13 @@
</widget>
</item>
<item>
+ <widget class="QLabel" name="label_file_path">
+ <property name="text">
+ <string/>
+ </property>
+ </widget>
+ </item>
+ <item>
<widget class="QListView" name="instanceView"/>
</item>
<item>
@@ -41,7 +48,7 @@
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
- <receiver>ImportResourcePackDialog</receiver>
+ <receiver>ImportResourceDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
@@ -57,7 +64,7 @@
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
- <receiver>ImportResourcePackDialog</receiver>
+ <receiver>ImportResourceDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
diff --git a/launcher/ui/dialogs/ImportResourcePackDialog.h b/launcher/ui/dialogs/ImportResourcePackDialog.h
deleted file mode 100644
index 8356f204..00000000
--- a/launcher/ui/dialogs/ImportResourcePackDialog.h
+++ /dev/null
@@ -1,27 +0,0 @@
-#pragma once
-
-#include <QDialog>
-#include <QItemSelection>
-
-#include "ui/instanceview/InstanceProxyModel.h"
-
-namespace Ui {
-class ImportResourcePackDialog;
-}
-
-class ImportResourcePackDialog : public QDialog {
- Q_OBJECT
-
- public:
- explicit ImportResourcePackDialog(QWidget* parent = 0);
- ~ImportResourcePackDialog();
- InstanceProxyModel* proxyModel;
- QString selectedInstanceKey;
-
- private:
- Ui::ImportResourcePackDialog* ui;
-
- private slots:
- void selectionChanged(QItemSelection, QItemSelection);
- void activated(QModelIndex);
-};
diff --git a/launcher/ui/dialogs/ModDownloadDialog.cpp b/launcher/ui/dialogs/ModDownloadDialog.cpp
deleted file mode 100644
index 24d23ba9..00000000
--- a/launcher/ui/dialogs/ModDownloadDialog.cpp
+++ /dev/null
@@ -1,202 +0,0 @@
-// SPDX-License-Identifier: GPL-3.0-only
-/*
- * 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
- * the Free Software Foundation, version 3.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
-
-#include "ModDownloadDialog.h"
-
-#include <BaseVersion.h>
-#include <InstanceList.h>
-#include <icons/IconList.h>
-
-#include "Application.h"
-#include "ReviewMessageBox.h"
-
-#include <QDialogButtonBox>
-#include <QLayout>
-#include <QPushButton>
-#include <QValidator>
-
-#include "ModDownloadTask.h"
-#include "ui/pages/modplatform/flame/FlameModPage.h"
-#include "ui/pages/modplatform/modrinth/ModrinthModPage.h"
-#include "ui/widgets/PageContainer.h"
-
-ModDownloadDialog::ModDownloadDialog(const std::shared_ptr<ModFolderModel>& mods, QWidget* parent, BaseInstance* instance)
- : QDialog(parent), mods(mods), m_verticalLayout(new QVBoxLayout(this)), m_instance(instance)
-{
- setObjectName(QStringLiteral("ModDownloadDialog"));
- m_verticalLayout->setObjectName(QStringLiteral("verticalLayout"));
-
- resize(std::max(0.5 * parent->width(), 400.0), std::max(0.75 * parent->height(), 400.0));
-
- setWindowIcon(APPLICATION->getThemedIcon("new"));
- // NOTE: m_buttons must be initialized before PageContainer, because it indirectly accesses m_buttons through setSuggestedPack! Do not
- // move this below.
- m_buttons = new QDialogButtonBox(QDialogButtonBox::Help | QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
-
- m_container = new PageContainer(this);
- m_container->setSizePolicy(QSizePolicy::Policy::Preferred, QSizePolicy::Policy::Expanding);
- m_container->layout()->setContentsMargins(0, 0, 0, 0);
- m_verticalLayout->addWidget(m_container);
-
- m_container->addButtons(m_buttons);
-
- connect(m_container, &PageContainer::selectedPageChanged, this, &ModDownloadDialog::selectedPageChanged);
-
- // Bonk Qt over its stupid head and make sure it understands which button is the default one...
- // See: https://stackoverflow.com/questions/24556831/qbuttonbox-set-default-button
- auto OkButton = m_buttons->button(QDialogButtonBox::Ok);
- OkButton->setEnabled(false);
- OkButton->setDefault(true);
- OkButton->setAutoDefault(true);
- OkButton->setText(tr("Review and confirm"));
- OkButton->setShortcut(tr("Ctrl+Return"));
- OkButton->setToolTip(tr("Opens a new popup to review your selected mods and confirm your selection. Shortcut: Ctrl+Return"));
- connect(OkButton, &QPushButton::clicked, this, &ModDownloadDialog::confirm);
-
- auto CancelButton = m_buttons->button(QDialogButtonBox::Cancel);
- CancelButton->setDefault(false);
- CancelButton->setAutoDefault(false);
- connect(CancelButton, &QPushButton::clicked, this, &ModDownloadDialog::reject);
-
- auto HelpButton = m_buttons->button(QDialogButtonBox::Help);
- HelpButton->setDefault(false);
- HelpButton->setAutoDefault(false);
- connect(HelpButton, &QPushButton::clicked, m_container, &PageContainer::help);
-
- QMetaObject::connectSlotsByName(this);
- setWindowModality(Qt::WindowModal);
- setWindowTitle(dialogTitle());
-
- restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("ModDownloadGeometry").toByteArray()));
-}
-
-QString ModDownloadDialog::dialogTitle()
-{
- return tr("Download mods");
-}
-
-void ModDownloadDialog::reject()
-{
- APPLICATION->settings()->set("ModDownloadGeometry", saveGeometry().toBase64());
- QDialog::reject();
-}
-
-void ModDownloadDialog::confirm()
-{
- auto keys = modTask.keys();
- keys.sort(Qt::CaseInsensitive);
-
- auto confirm_dialog = ReviewMessageBox::create(this, tr("Confirm mods to download"));
-
- for (auto& task : keys) {
- confirm_dialog->appendMod({ task, modTask.find(task).value()->getFilename() });
- }
-
- if (confirm_dialog->exec()) {
- auto deselected = confirm_dialog->deselectedMods();
- for (auto name : deselected) {
- modTask.remove(name);
- }
-
- this->accept();
- }
-}
-
-void ModDownloadDialog::accept()
-{
- APPLICATION->settings()->set("ModDownloadGeometry", saveGeometry().toBase64());
- QDialog::accept();
-}
-
-QList<BasePage*> ModDownloadDialog::getPages()
-{
- QList<BasePage*> pages;
-
- pages.append(ModrinthModPage::create(this, m_instance));
- if (APPLICATION->capabilities() & Application::SupportsFlame)
- pages.append(FlameModPage::create(this, m_instance));
-
- m_selectedPage = dynamic_cast<ModPage*>(pages[0]);
-
- return pages;
-}
-
-void ModDownloadDialog::addSelectedMod(QString name, ModDownloadTask* task)
-{
- removeSelectedMod(name);
- modTask.insert(name, task);
-
- m_buttons->button(QDialogButtonBox::Ok)->setEnabled(!modTask.isEmpty());
-}
-
-void ModDownloadDialog::removeSelectedMod(QString name)
-{
- if (modTask.contains(name))
- delete modTask.find(name).value();
- modTask.remove(name);
-
- m_buttons->button(QDialogButtonBox::Ok)->setEnabled(!modTask.isEmpty());
-}
-
-bool ModDownloadDialog::isModSelected(QString name, QString filename) const
-{
- // FIXME: Is there a way to check for versions without checking the filename
- // as a heuristic, other than adding such info to ModDownloadTask itself?
- auto iter = modTask.find(name);
- return iter != modTask.end() && (iter.value()->getFilename() == filename);
-}
-
-bool ModDownloadDialog::isModSelected(QString name) const
-{
- auto iter = modTask.find(name);
- return iter != modTask.end();
-}
-
-const QList<ModDownloadTask*> ModDownloadDialog::getTasks()
-{
- return modTask.values();
-}
-
-void ModDownloadDialog::selectedPageChanged(BasePage* previous, BasePage* selected)
-{
- auto* prev_page = dynamic_cast<ModPage*>(previous);
- if (!prev_page) {
- qCritical() << "Page '" << previous->displayName() << "' in ModDownloadDialog is not a ModPage!";
- return;
- }
-
- 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
- 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
deleted file mode 100644
index fcf6f4fc..00000000
--- a/launcher/ui/dialogs/ModDownloadDialog.h
+++ /dev/null
@@ -1,78 +0,0 @@
-// SPDX-License-Identifier: GPL-3.0-only
-/*
- * 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
- * the Free Software Foundation, version 3.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
-
-#pragma once
-
-#include <QDialog>
-#include <QVBoxLayout>
-
-#include "ModDownloadTask.h"
-#include "minecraft/mod/ModFolderModel.h"
-#include "ui/pages/BasePageProvider.h"
-
-namespace Ui
-{
-class ModDownloadDialog;
-}
-
-class PageContainer;
-class QDialogButtonBox;
-class ModPage;
-class ModrinthModPage;
-
-class ModDownloadDialog final : public QDialog, public BasePageProvider
-{
- Q_OBJECT
-
- public:
- explicit ModDownloadDialog(const std::shared_ptr<ModFolderModel>& mods, QWidget* parent, BaseInstance* instance);
- ~ModDownloadDialog() override = default;
-
- QString dialogTitle() override;
- QList<BasePage*> getPages() override;
-
- void addSelectedMod(QString name = QString(), ModDownloadTask* task = nullptr);
- void removeSelectedMod(QString name = QString());
- bool isModSelected(QString name, QString filename) const;
- bool isModSelected(QString name) const;
-
- const QList<ModDownloadTask*> getTasks();
- const std::shared_ptr<ModFolderModel>& mods;
-
- bool selectPage(QString pageId);
- ModPage* getSelectedPage();
-
- public slots:
- void confirm();
- void accept() override;
- void reject() override;
-
- 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;
- ModPage* m_selectedPage = nullptr;
-
- QHash<QString, ModDownloadTask*> modTask;
- BaseInstance* m_instance;
-};
diff --git a/launcher/ui/dialogs/ModUpdateDialog.cpp b/launcher/ui/dialogs/ModUpdateDialog.cpp
index 2704243e..8618b924 100644
--- a/launcher/ui/dialogs/ModUpdateDialog.cpp
+++ b/launcher/ui/dialogs/ModUpdateDialog.cpp
@@ -21,6 +21,8 @@
#include <QTextBrowser>
#include <QTreeWidgetItem>
+#include <optional>
+
static ModPlatform::ProviderCapabilities ProviderCaps;
static std::list<Version> mcVersions(BaseInstance* inst)
@@ -28,7 +30,7 @@ static std::list<Version> mcVersions(BaseInstance* inst)
return { static_cast<MinecraftInstance*>(inst)->getPackProfile()->getComponent("net.minecraft")->getVersion() };
}
-static ModAPI::ModLoaderTypes mcLoaders(BaseInstance* inst)
+static std::optional<ResourceAPI::ModLoaderTypes> mcLoaders(BaseInstance* inst)
{
return { static_cast<MinecraftInstance*>(inst)->getPackProfile()->getModLoaders() };
}
@@ -86,15 +88,15 @@ void ModUpdateDialog::checkCandidates()
SequentialTask check_task(m_parent, tr("Checking for updates"));
if (!m_modrinth_to_update.empty()) {
- m_modrinth_check_task = new ModrinthCheckUpdate(m_modrinth_to_update, versions, loaders, m_mod_model);
- connect(m_modrinth_check_task, &CheckUpdateTask::checkFailed, this,
+ m_modrinth_check_task.reset(new ModrinthCheckUpdate(m_modrinth_to_update, versions, loaders, m_mod_model));
+ connect(m_modrinth_check_task.get(), &CheckUpdateTask::checkFailed, this,
[this](Mod* mod, QString reason, QUrl recover_url) { m_failed_check_update.append({mod, reason, recover_url}); });
check_task.addTask(m_modrinth_check_task);
}
if (!m_flame_to_update.empty()) {
- m_flame_check_task = new FlameCheckUpdate(m_flame_to_update, versions, loaders, m_mod_model);
- connect(m_flame_check_task, &CheckUpdateTask::checkFailed, this,
+ m_flame_check_task.reset(new FlameCheckUpdate(m_flame_to_update, versions, loaders, m_mod_model));
+ connect(m_flame_check_task.get(), &CheckUpdateTask::checkFailed, this,
[this](Mod* mod, QString reason, QUrl recover_url) { m_failed_check_update.append({mod, reason, recover_url}); });
check_task.addTask(m_flame_check_task);
}
@@ -212,14 +214,14 @@ auto ModUpdateDialog::ensureMetadata() -> bool
bool confirm_rest = false;
bool try_others_rest = false;
bool skip_rest = false;
- ModPlatform::Provider provider_rest = ModPlatform::Provider::MODRINTH;
+ ModPlatform::ResourceProvider provider_rest = ModPlatform::ResourceProvider::MODRINTH;
- auto addToTmp = [&](Mod* m, ModPlatform::Provider p) {
+ auto addToTmp = [&](Mod* m, ModPlatform::ResourceProvider p) {
switch (p) {
- case ModPlatform::Provider::MODRINTH:
+ case ModPlatform::ResourceProvider::MODRINTH:
modrinth_tmp.push_back(m);
break;
- case ModPlatform::Provider::FLAME:
+ case ModPlatform::ResourceProvider::FLAME:
flame_tmp.push_back(m);
break;
}
@@ -264,10 +266,10 @@ auto ModUpdateDialog::ensureMetadata() -> bool
}
if (!modrinth_tmp.empty()) {
- auto* modrinth_task = new EnsureMetadataTask(modrinth_tmp, index_dir, ModPlatform::Provider::MODRINTH);
- connect(modrinth_task, &EnsureMetadataTask::metadataReady, [this](Mod* candidate) { onMetadataEnsured(candidate); });
- connect(modrinth_task, &EnsureMetadataTask::metadataFailed, [this, &should_try_others](Mod* candidate) {
- onMetadataFailed(candidate, should_try_others.find(candidate->internal_id()).value(), ModPlatform::Provider::MODRINTH);
+ auto modrinth_task = makeShared<EnsureMetadataTask>(modrinth_tmp, index_dir, ModPlatform::ResourceProvider::MODRINTH);
+ connect(modrinth_task.get(), &EnsureMetadataTask::metadataReady, [this](Mod* candidate) { onMetadataEnsured(candidate); });
+ connect(modrinth_task.get(), &EnsureMetadataTask::metadataFailed, [this, &should_try_others](Mod* candidate) {
+ onMetadataFailed(candidate, should_try_others.find(candidate->internal_id()).value(), ModPlatform::ResourceProvider::MODRINTH);
});
if (modrinth_task->getHashingTask())
@@ -277,10 +279,10 @@ auto ModUpdateDialog::ensureMetadata() -> bool
}
if (!flame_tmp.empty()) {
- auto* flame_task = new EnsureMetadataTask(flame_tmp, index_dir, ModPlatform::Provider::FLAME);
- connect(flame_task, &EnsureMetadataTask::metadataReady, [this](Mod* candidate) { onMetadataEnsured(candidate); });
- connect(flame_task, &EnsureMetadataTask::metadataFailed, [this, &should_try_others](Mod* candidate) {
- onMetadataFailed(candidate, should_try_others.find(candidate->internal_id()).value(), ModPlatform::Provider::FLAME);
+ auto flame_task = makeShared<EnsureMetadataTask>(flame_tmp, index_dir, ModPlatform::ResourceProvider::FLAME);
+ connect(flame_task.get(), &EnsureMetadataTask::metadataReady, [this](Mod* candidate) { onMetadataEnsured(candidate); });
+ connect(flame_task.get(), &EnsureMetadataTask::metadataFailed, [this, &should_try_others](Mod* candidate) {
+ onMetadataFailed(candidate, should_try_others.find(candidate->internal_id()).value(), ModPlatform::ResourceProvider::FLAME);
});
if (flame_task->getHashingTask())
@@ -306,35 +308,35 @@ void ModUpdateDialog::onMetadataEnsured(Mod* mod)
return;
switch (mod->metadata()->provider) {
- case ModPlatform::Provider::MODRINTH:
+ case ModPlatform::ResourceProvider::MODRINTH:
m_modrinth_to_update.push_back(mod);
break;
- case ModPlatform::Provider::FLAME:
+ case ModPlatform::ResourceProvider::FLAME:
m_flame_to_update.push_back(mod);
break;
}
}
-ModPlatform::Provider next(ModPlatform::Provider p)
+ModPlatform::ResourceProvider next(ModPlatform::ResourceProvider p)
{
switch (p) {
- case ModPlatform::Provider::MODRINTH:
- return ModPlatform::Provider::FLAME;
- case ModPlatform::Provider::FLAME:
- return ModPlatform::Provider::MODRINTH;
+ case ModPlatform::ResourceProvider::MODRINTH:
+ return ModPlatform::ResourceProvider::FLAME;
+ case ModPlatform::ResourceProvider::FLAME:
+ return ModPlatform::ResourceProvider::MODRINTH;
}
- return ModPlatform::Provider::FLAME;
+ return ModPlatform::ResourceProvider::FLAME;
}
-void ModUpdateDialog::onMetadataFailed(Mod* mod, bool try_others, ModPlatform::Provider first_choice)
+void ModUpdateDialog::onMetadataFailed(Mod* mod, bool try_others, ModPlatform::ResourceProvider first_choice)
{
if (try_others) {
auto index_dir = indexDir();
- auto* task = new EnsureMetadataTask(mod, index_dir, next(first_choice));
- connect(task, &EnsureMetadataTask::metadataReady, [this](Mod* candidate) { onMetadataEnsured(candidate); });
- connect(task, &EnsureMetadataTask::metadataFailed, [this](Mod* candidate) { onMetadataFailed(candidate, false); });
+ auto task = makeShared<EnsureMetadataTask>(mod, index_dir, next(first_choice));
+ connect(task.get(), &EnsureMetadataTask::metadataReady, [this](Mod* candidate) { onMetadataEnsured(candidate); });
+ connect(task.get(), &EnsureMetadataTask::metadataFailed, [this](Mod* candidate) { onMetadataFailed(candidate, false); });
m_second_try_metadata->addTask(task);
} else {
@@ -368,7 +370,7 @@ void ModUpdateDialog::appendMod(CheckUpdateTask::UpdatableMod const& info)
QString text = info.changelog;
switch (info.provider) {
- case ModPlatform::Provider::MODRINTH: {
+ case ModPlatform::ResourceProvider::MODRINTH: {
text = markdownToHTML(info.changelog.toUtf8());
break;
}
@@ -386,9 +388,9 @@ void ModUpdateDialog::appendMod(CheckUpdateTask::UpdatableMod const& info)
ui->modTreeWidget->addTopLevelItem(item_top);
}
-auto ModUpdateDialog::getTasks() -> const QList<ModDownloadTask*>
+auto ModUpdateDialog::getTasks() -> const QList<ResourceDownloadTask::Ptr>
{
- QList<ModDownloadTask*> list;
+ QList<ResourceDownloadTask::Ptr> list;
auto* item = ui->modTreeWidget->topLevelItem(0);
diff --git a/launcher/ui/dialogs/ModUpdateDialog.h b/launcher/ui/dialogs/ModUpdateDialog.h
index bd486f0d..1a92f613 100644
--- a/launcher/ui/dialogs/ModUpdateDialog.h
+++ b/launcher/ui/dialogs/ModUpdateDialog.h
@@ -1,7 +1,7 @@
#pragma once
#include "BaseInstance.h"
-#include "ModDownloadTask.h"
+#include "ResourceDownloadTask.h"
#include "ReviewMessageBox.h"
#include "minecraft/mod/ModFolderModel.h"
@@ -25,7 +25,7 @@ class ModUpdateDialog final : public ReviewMessageBox {
void appendMod(const CheckUpdateTask::UpdatableMod& info);
- const QList<ModDownloadTask*> getTasks();
+ const QList<ResourceDownloadTask::Ptr> getTasks();
auto indexDir() const -> QDir { return m_mod_model->indexDir(); }
auto noUpdates() const -> bool { return m_no_updates; };
@@ -36,13 +36,13 @@ class ModUpdateDialog final : public ReviewMessageBox {
private slots:
void onMetadataEnsured(Mod*);
- void onMetadataFailed(Mod*, bool try_others = false, ModPlatform::Provider first_choice = ModPlatform::Provider::MODRINTH);
+ void onMetadataFailed(Mod*, bool try_others = false, ModPlatform::ResourceProvider first_choice = ModPlatform::ResourceProvider::MODRINTH);
private:
QWidget* m_parent;
- ModrinthCheckUpdate* m_modrinth_check_task = nullptr;
- FlameCheckUpdate* m_flame_check_task = nullptr;
+ shared_qobject_ptr<ModrinthCheckUpdate> m_modrinth_check_task;
+ shared_qobject_ptr<FlameCheckUpdate> m_flame_check_task;
const std::shared_ptr<ModFolderModel> m_mod_model;
@@ -50,11 +50,11 @@ class ModUpdateDialog final : public ReviewMessageBox {
QList<Mod*> m_modrinth_to_update;
QList<Mod*> m_flame_to_update;
- ConcurrentTask* m_second_try_metadata;
+ ConcurrentTask::Ptr m_second_try_metadata;
QList<std::tuple<Mod*, QString>> m_failed_metadata;
QList<std::tuple<Mod*, QString, QUrl>> m_failed_check_update;
- QHash<QString, ModDownloadTask*> m_tasks;
+ QHash<QString, ResourceDownloadTask::Ptr> m_tasks;
BaseInstance* m_instance;
bool m_no_updates = false;
diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.cpp b/launcher/ui/dialogs/ResourceDownloadDialog.cpp
new file mode 100644
index 00000000..fa829bfb
--- /dev/null
+++ b/launcher/ui/dialogs/ResourceDownloadDialog.cpp
@@ -0,0 +1,232 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * 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
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "ResourceDownloadDialog.h"
+
+#include <QPushButton>
+
+#include "Application.h"
+#include "ResourceDownloadTask.h"
+
+#include "minecraft/mod/ModFolderModel.h"
+
+#include "ui/dialogs/ReviewMessageBox.h"
+
+#include "ui/pages/modplatform/ResourcePage.h"
+
+#include "ui/pages/modplatform/flame/FlameResourcePages.h"
+#include "ui/pages/modplatform/modrinth/ModrinthResourcePages.h"
+
+#include "ui/widgets/PageContainer.h"
+
+namespace ResourceDownload {
+
+ResourceDownloadDialog::ResourceDownloadDialog(QWidget* parent, const std::shared_ptr<ResourceFolderModel> base_model)
+ : QDialog(parent), m_base_model(base_model), m_buttons(QDialogButtonBox::Help | QDialogButtonBox::Ok | QDialogButtonBox::Cancel), m_vertical_layout(this)
+{
+ setObjectName(QStringLiteral("ResourceDownloadDialog"));
+
+ resize(std::max(0.5 * parent->width(), 400.0), std::max(0.75 * parent->height(), 400.0));
+
+ setWindowIcon(APPLICATION->getThemedIcon("new"));
+
+ // Bonk Qt over its stupid head and make sure it understands which button is the default one...
+ // See: https://stackoverflow.com/questions/24556831/qbuttonbox-set-default-button
+ auto OkButton = m_buttons.button(QDialogButtonBox::Ok);
+ OkButton->setEnabled(false);
+ OkButton->setDefault(true);
+ OkButton->setAutoDefault(true);
+ OkButton->setText(tr("Review and confirm"));
+ OkButton->setShortcut(tr("Ctrl+Return"));
+
+ auto CancelButton = m_buttons.button(QDialogButtonBox::Cancel);
+ CancelButton->setDefault(false);
+ CancelButton->setAutoDefault(false);
+
+ auto HelpButton = m_buttons.button(QDialogButtonBox::Help);
+ HelpButton->setDefault(false);
+ HelpButton->setAutoDefault(false);
+
+ setWindowModality(Qt::WindowModal);
+}
+
+void ResourceDownloadDialog::accept()
+{
+ if (!geometrySaveKey().isEmpty())
+ APPLICATION->settings()->set(geometrySaveKey(), saveGeometry().toBase64());
+
+ QDialog::accept();
+}
+
+void ResourceDownloadDialog::reject()
+{
+ if (!geometrySaveKey().isEmpty())
+ APPLICATION->settings()->set(geometrySaveKey(), saveGeometry().toBase64());
+
+ QDialog::reject();
+}
+
+// NOTE: We can't have this in the ctor because PageContainer calls a virtual function, and so
+// won't work with subclasses if we put it in this ctor.
+void ResourceDownloadDialog::initializeContainer()
+{
+ m_container = new PageContainer(this);
+ m_container->setSizePolicy(QSizePolicy::Policy::Preferred, QSizePolicy::Policy::Expanding);
+ m_container->layout()->setContentsMargins(0, 0, 0, 0);
+ m_vertical_layout.addWidget(m_container);
+
+ m_container->addButtons(&m_buttons);
+
+ connect(m_container, &PageContainer::selectedPageChanged, this, &ResourceDownloadDialog::selectedPageChanged);
+}
+
+void ResourceDownloadDialog::connectButtons()
+{
+ auto OkButton = m_buttons.button(QDialogButtonBox::Ok);
+ OkButton->setToolTip(tr("Opens a new popup to review your selected %1 and confirm your selection. Shortcut: Ctrl+Return").arg(resourcesString()));
+ connect(OkButton, &QPushButton::clicked, this, &ResourceDownloadDialog::confirm);
+
+ auto CancelButton = m_buttons.button(QDialogButtonBox::Cancel);
+ connect(CancelButton, &QPushButton::clicked, this, &ResourceDownloadDialog::reject);
+
+ auto HelpButton = m_buttons.button(QDialogButtonBox::Help);
+ connect(HelpButton, &QPushButton::clicked, m_container, &PageContainer::help);
+}
+
+void ResourceDownloadDialog::confirm()
+{
+ auto keys = m_selected.keys();
+ keys.sort(Qt::CaseInsensitive);
+
+ auto confirm_dialog = ReviewMessageBox::create(this, tr("Confirm %1 to download").arg(resourcesString()));
+ confirm_dialog->retranslateUi(resourcesString());
+
+ for (auto& task : keys) {
+ auto selected = m_selected.constFind(task).value();
+ confirm_dialog->appendResource({ task, selected->getFilename(), selected->getCustomPath() });
+ }
+
+ if (confirm_dialog->exec()) {
+ auto deselected = confirm_dialog->deselectedResources();
+ for (auto name : deselected) {
+ m_selected.remove(name);
+ }
+
+ this->accept();
+ }
+}
+
+bool ResourceDownloadDialog::selectPage(QString pageId)
+{
+ return m_container->selectPage(pageId);
+}
+
+ResourcePage* ResourceDownloadDialog::getSelectedPage()
+{
+ return m_selectedPage;
+}
+
+void ResourceDownloadDialog::addResource(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, bool is_indexed)
+{
+ removeResource(pack, ver);
+
+ ver.is_currently_selected = true;
+ m_selected.insert(pack.name, makeShared<ResourceDownloadTask>(pack, ver, getBaseModel(), is_indexed));
+
+ m_buttons.button(QDialogButtonBox::Ok)->setEnabled(!m_selected.isEmpty());
+}
+
+static ModPlatform::IndexedVersion& getVersionWithID(ModPlatform::IndexedPack& pack, QVariant id)
+{
+ Q_ASSERT(pack.versionsLoaded);
+ auto it = std::find_if(pack.versions.begin(), pack.versions.end(), [id](auto const& v) { return v.fileId == id; });
+ Q_ASSERT(it != pack.versions.end());
+ return *it;
+}
+
+void ResourceDownloadDialog::removeResource(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver)
+{
+ if (auto selected_task_it = m_selected.find(pack.name); selected_task_it != m_selected.end()) {
+ auto selected_task = *selected_task_it;
+ auto old_version_id = selected_task->getVersionID();
+
+ // If the new and old version IDs don't match, search for the old one and deselect it.
+ if (ver.fileId != old_version_id)
+ getVersionWithID(pack, old_version_id).is_currently_selected = false;
+ }
+
+ // Deselect the new version too, since all versions of that pack got removed.
+ ver.is_currently_selected = false;
+
+ m_selected.remove(pack.name);
+
+ m_buttons.button(QDialogButtonBox::Ok)->setEnabled(!m_selected.isEmpty());
+}
+
+const QList<ResourceDownloadDialog::DownloadTaskPtr> ResourceDownloadDialog::getTasks()
+{
+ return m_selected.values();
+}
+
+void ResourceDownloadDialog::selectedPageChanged(BasePage* previous, BasePage* selected)
+{
+ auto* prev_page = dynamic_cast<ResourcePage*>(previous);
+ if (!prev_page) {
+ qCritical() << "Page '" << previous->displayName() << "' in ResourceDownloadDialog is not a ResourcePage!";
+ return;
+ }
+
+ m_selectedPage = dynamic_cast<ResourcePage*>(selected);
+ if (!m_selectedPage) {
+ qCritical() << "Page '" << selected->displayName() << "' in ResourceDownloadDialog is not a ResourcePage!";
+ return;
+ }
+
+ // Same effect as having a global search bar
+ m_selectedPage->setSearchTerm(prev_page->getSearchTerm());
+}
+
+
+
+ModDownloadDialog::ModDownloadDialog(QWidget* parent, const std::shared_ptr<ModFolderModel>& mods, BaseInstance* instance)
+ : ResourceDownloadDialog(parent, mods), m_instance(instance)
+{
+ setWindowTitle(dialogTitle());
+
+ initializeContainer();
+ connectButtons();
+
+ if (!geometrySaveKey().isEmpty())
+ restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get(geometrySaveKey()).toByteArray()));
+}
+
+QList<BasePage*> ModDownloadDialog::getPages()
+{
+ QList<BasePage*> pages;
+
+ pages.append(ModrinthModPage::create(this, *m_instance));
+ if (APPLICATION->capabilities() & Application::SupportsFlame)
+ pages.append(FlameModPage::create(this, *m_instance));
+
+ m_selectedPage = dynamic_cast<ModPage*>(pages[0]);
+
+ return pages;
+}
+
+} // namespace ResourceDownload
diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.h b/launcher/ui/dialogs/ResourceDownloadDialog.h
new file mode 100644
index 00000000..19843532
--- /dev/null
+++ b/launcher/ui/dialogs/ResourceDownloadDialog.h
@@ -0,0 +1,111 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * 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
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <QDialog>
+#include <QDialogButtonBox>
+#include <QHash>
+#include <QLayout>
+
+#include "QObjectPtr.h"
+#include "modplatform/ModIndex.h"
+#include "ui/pages/BasePageProvider.h"
+
+class BaseInstance;
+class ModFolderModel;
+class PageContainer;
+class QVBoxLayout;
+class QDialogButtonBox;
+class ResourceDownloadTask;
+class ResourceFolderModel;
+
+namespace ResourceDownload {
+
+class ResourcePage;
+
+class ResourceDownloadDialog : public QDialog, public BasePageProvider {
+ Q_OBJECT
+
+ public:
+ using DownloadTaskPtr = shared_qobject_ptr<ResourceDownloadTask>;
+
+ ResourceDownloadDialog(QWidget* parent, const std::shared_ptr<ResourceFolderModel> base_model);
+
+ void initializeContainer();
+ void connectButtons();
+
+ //: String that gets appended to the download dialog title ("Download " + resourcesString())
+ [[nodiscard]] virtual QString resourcesString() const { return tr("resources"); }
+
+ QString dialogTitle() override { return tr("Download %1").arg(resourcesString()); };
+
+ bool selectPage(QString pageId);
+ ResourcePage* getSelectedPage();
+
+ void addResource(ModPlatform::IndexedPack&, ModPlatform::IndexedVersion&, bool is_indexed = false);
+ void removeResource(ModPlatform::IndexedPack&, ModPlatform::IndexedVersion&);
+
+ const QList<DownloadTaskPtr> getTasks();
+ [[nodiscard]] const std::shared_ptr<ResourceFolderModel> getBaseModel() const { return m_base_model; }
+
+ public slots:
+ void accept() override;
+ void reject() override;
+
+ protected slots:
+ void selectedPageChanged(BasePage* previous, BasePage* selected);
+
+ virtual void confirm();
+
+ protected:
+ [[nodiscard]] virtual QString geometrySaveKey() const { return ""; }
+
+ protected:
+ const std::shared_ptr<ResourceFolderModel> m_base_model;
+
+ PageContainer* m_container = nullptr;
+ ResourcePage* m_selectedPage = nullptr;
+
+ QDialogButtonBox m_buttons;
+ QVBoxLayout m_vertical_layout;
+
+ QHash<QString, DownloadTaskPtr> m_selected;
+};
+
+
+
+class ModDownloadDialog final : public ResourceDownloadDialog {
+ Q_OBJECT
+
+ public:
+ explicit ModDownloadDialog(QWidget* parent, const std::shared_ptr<ModFolderModel>& mods, BaseInstance* instance);
+ ~ModDownloadDialog() override = default;
+
+ //: String that gets appended to the mod download dialog title ("Download " + resourcesString())
+ [[nodiscard]] QString resourcesString() const override { return tr("mods"); }
+ [[nodiscard]] QString geometrySaveKey() const override { return "ModDownloadGeometry"; }
+
+ QList<BasePage*> getPages() override;
+
+ private:
+ BaseInstance* m_instance;
+};
+
+} // namespace ResourceDownload
diff --git a/launcher/ui/dialogs/ReviewMessageBox.cpp b/launcher/ui/dialogs/ReviewMessageBox.cpp
index 7c25c91c..7b2df278 100644
--- a/launcher/ui/dialogs/ReviewMessageBox.cpp
+++ b/launcher/ui/dialogs/ReviewMessageBox.cpp
@@ -1,6 +1,8 @@
#include "ReviewMessageBox.h"
#include "ui_ReviewMessageBox.h"
+#include "Application.h"
+
#include <QPushButton>
ReviewMessageBox::ReviewMessageBox(QWidget* parent, QString const& title, QString const& icon)
@@ -11,6 +13,10 @@ ReviewMessageBox::ReviewMessageBox(QWidget* parent, QString const& title, QStrin
auto back_button = ui->buttonBox->button(QDialogButtonBox::Cancel);
back_button->setText(tr("Back"));
+ ui->modTreeWidget->header()->setSectionResizeMode(0, QHeaderView::Stretch);
+ ui->modTreeWidget->header()->setStretchLastSection(false);
+ ui->modTreeWidget->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
+
connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &ReviewMessageBox::accept);
connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &ReviewMessageBox::reject);
}
@@ -25,7 +31,7 @@ auto ReviewMessageBox::create(QWidget* parent, QString&& title, QString&& icon)
return new ReviewMessageBox(parent, title, icon);
}
-void ReviewMessageBox::appendMod(ModInformation&& info)
+void ReviewMessageBox::appendResource(ResourceInformation&& info)
{
auto itemTop = new QTreeWidgetItem(ui->modTreeWidget);
itemTop->setCheckState(0, Qt::CheckState::Checked);
@@ -36,10 +42,20 @@ void ReviewMessageBox::appendMod(ModInformation&& info)
itemTop->insertChildren(0, { filenameItem });
+ if (!info.custom_file_path.isEmpty()) {
+ auto customPathItem = new QTreeWidgetItem(itemTop);
+ customPathItem->setText(0, tr("This download will be placed in: %1").arg(info.custom_file_path));
+
+ itemTop->insertChildren(1, { customPathItem });
+
+ itemTop->setIcon(1, QIcon(APPLICATION->getThemedIcon("status-yellow")));
+ itemTop->setToolTip(1, tr("This file will be downloaded to a folder location different from the default, possibly due to its loader requiring it."));
+ }
+
ui->modTreeWidget->addTopLevelItem(itemTop);
}
-auto ReviewMessageBox::deselectedMods() -> QStringList
+auto ReviewMessageBox::deselectedResources() -> QStringList
{
QStringList list;
@@ -55,3 +71,11 @@ auto ReviewMessageBox::deselectedMods() -> QStringList
return list;
}
+
+void ReviewMessageBox::retranslateUi(QString resources_name)
+{
+ setWindowTitle(tr("Confirm %1 selection").arg(resources_name));
+
+ ui->explainLabel->setText(tr("You're about to download the following %1:").arg(resources_name));
+ ui->onlyCheckedLabel->setText(tr("Only %1 with a check will be downloaded!").arg(resources_name));
+}
diff --git a/launcher/ui/dialogs/ReviewMessageBox.h b/launcher/ui/dialogs/ReviewMessageBox.h
index 9cfa679a..5ec2bc23 100644
--- a/launcher/ui/dialogs/ReviewMessageBox.h
+++ b/launcher/ui/dialogs/ReviewMessageBox.h
@@ -12,15 +12,18 @@ class ReviewMessageBox : public QDialog {
public:
static auto create(QWidget* parent, QString&& title, QString&& icon = "") -> ReviewMessageBox*;
- using ModInformation = struct {
+ using ResourceInformation = struct res_info {
QString name;
QString filename;
+ QString custom_file_path {};
};
- void appendMod(ModInformation&& info);
- auto deselectedMods() -> QStringList;
+ void appendResource(ResourceInformation&& info);
+ auto deselectedResources() -> QStringList;
- ~ReviewMessageBox();
+ void retranslateUi(QString resources_name);
+
+ ~ReviewMessageBox() override;
protected:
ReviewMessageBox(QWidget* parent, const QString& title, const QString& icon);
diff --git a/launcher/ui/dialogs/ReviewMessageBox.ui b/launcher/ui/dialogs/ReviewMessageBox.ui
index ab3bcc2f..bf53ae80 100644
--- a/launcher/ui/dialogs/ReviewMessageBox.ui
+++ b/launcher/ui/dialogs/ReviewMessageBox.ui
@@ -10,9 +10,6 @@
<height>350</height>
</rect>
</property>
- <property name="windowTitle">
- <string>Confirm mod selection</string>
- </property>
<property name="sizeGripEnabled">
<bool>true</bool>
</property>
@@ -39,22 +36,21 @@
<string/>
</property>
</column>
+ <column>
+ <property name="text">
+ <string/>
+ </property>
+ </column>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="explainLabel">
- <property name="text">
- <string>You're about to download the following mods:</string>
- </property>
</widget>
</item>
<item row="5" column="0" rowspan="2">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="onlyCheckedLabel">
- <property name="text">
- <string>Only mods with a check will be downloaded!</string>
- </property>
</widget>
</item>
<item>
diff --git a/launcher/ui/dialogs/UpdateDialog.cpp b/launcher/ui/dialogs/UpdateDialog.cpp
deleted file mode 100644
index 349d768f..00000000
--- a/launcher/ui/dialogs/UpdateDialog.cpp
+++ /dev/null
@@ -1,216 +0,0 @@
-// SPDX-License-Identifier: GPL-3.0-only
-/*
- * PolyMC - Minecraft Launcher
- * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- *
- * This file incorporates work covered by the following copyright and
- * permission notice:
- *
- * Copyright 2013-2021 MultiMC Contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include "UpdateDialog.h"
-#include "ui_UpdateDialog.h"
-#include <QDebug>
-#include "Application.h"
-#include <settings/SettingsObject.h>
-#include <Json.h>
-
-#include "BuildConfig.h"
-#include "Markdown.h"
-
-UpdateDialog::UpdateDialog(bool hasUpdate, QWidget *parent) : QDialog(parent), ui(new Ui::UpdateDialog)
-{
- ui->setupUi(this);
- auto channel = APPLICATION->settings()->get("UpdateChannel").toString();
- if(hasUpdate)
- {
- ui->label->setText(tr("A new %1 update is available!").arg(channel));
- }
- else
- {
- ui->label->setText(tr("No %1 updates found. You are running the latest version.").arg(channel));
- ui->btnUpdateNow->setHidden(true);
- ui->btnUpdateLater->setText(tr("Close"));
- }
- ui->changelogBrowser->setHtml(tr("<center><h1>Loading changelog...</h1></center>"));
- loadChangelog();
- restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("UpdateDialogGeometry").toByteArray()));
-}
-
-UpdateDialog::~UpdateDialog()
-{
-}
-
-void UpdateDialog::loadChangelog()
-{
- auto channel = APPLICATION->settings()->get("UpdateChannel").toString();
- dljob = new NetJob("Changelog", APPLICATION->network());
- QString url;
- if(channel == "stable")
- {
- url = QString("https://raw.githubusercontent.com/PrismLauncher/PrismLauncher/%1/changelog.md").arg(channel);
- m_changelogType = CHANGELOG_MARKDOWN;
- }
- else
- {
- url = QString("https://api.github.com/repos/PrismLauncher/PrismLauncher/compare/%1...%2").arg(BuildConfig.GIT_COMMIT, channel);
- m_changelogType = CHANGELOG_COMMITS;
- }
- dljob->addNetAction(Net::Download::makeByteArray(QUrl(url), &changelogData));
- connect(dljob.get(), &NetJob::succeeded, this, &UpdateDialog::changelogLoaded);
- connect(dljob.get(), &NetJob::failed, this, &UpdateDialog::changelogFailed);
- dljob->start();
-}
-
-QString reprocessMarkdown(QByteArray markdown)
-{
- QString output = markdownToHTML(markdown);
-
- // HACK: easier than customizing hoedown
- output.replace(QRegularExpression("GH-([0-9]+)"), "<a href=\"https://github.com/PrismLauncher/PrismLauncher/issues/\\1\">GH-\\1</a>");
- qDebug() << output;
- return output;
-}
-
-QString reprocessCommits(QByteArray json)
-{
- auto channel = APPLICATION->settings()->get("UpdateChannel").toString();
- try
- {
- QString result;
- auto document = Json::requireDocument(json);
- auto rootobject = Json::requireObject(document);
- auto status = Json::requireString(rootobject, "status");
- auto diff_url = Json::requireString(rootobject, "html_url");
-
- auto print_commits = [&]()
- {
- result += "<table cellspacing=0 cellpadding=2 style='border-width: 1px; border-style: solid'>";
- auto commitarray = Json::requireArray(rootobject, "commits");
- for(int i = commitarray.size() - 1; i >= 0; i--)
- {
- const auto & commitval = commitarray[i];
- auto commitobj = Json::requireObject(commitval);
- auto parents_info = Json::ensureArray(commitobj, "parents");
- // NOTE: this ignores merge commits, because they have more than one parent
- if(parents_info.size() > 1)
- {
- continue;
- }
- auto commit_url = Json::requireString(commitobj, "html_url");
- auto commit_info = Json::requireObject(commitobj, "commit");
- auto commit_message = Json::requireString(commit_info, "message");
- auto lines = commit_message.split('\n');
- QRegularExpression regexp("(?<prefix>(GH-(?<issuenr>[0-9]+))|(NOISSUE)|(SCRATCH))? *(?<rest>.*) *");
- auto match = regexp.match(lines.takeFirst(), 0, QRegularExpression::NormalMatch);
- auto issuenr = match.captured("issuenr");
- auto prefix = match.captured("prefix");
- auto rest = match.captured("rest");
- result += "<tr><td>";
- if(issuenr.length())
- {
- result += QString("<a href=\"https://github.com/PrismLauncher/PrismLauncher/issues/%1\">GH-%2</a>").arg(issuenr, issuenr);
- }
- else if(prefix.length())
- {
- result += QString("<a href=\"%1\">%2</a>").arg(commit_url, prefix);
- }
- else
- {
- result += QString("<a href=\"%1\">NOISSUE</a>").arg(commit_url);
- }
- result += "</td>";
- lines.prepend(rest);
- result += "<td><p>" + lines.join("<br />") + "</p></td></tr>";
- }
- result += "</table>";
- };
-
- if(status == "identical")
- {
- return QObject::tr("<p>There are no code changes between your current version and latest %1.</p>").arg(channel);
- }
- else if(status == "ahead")
- {
- result += QObject::tr("<p>Following commits were added since last update:</p>");
- print_commits();
- }
- else if(status == "diverged")
- {
- auto commit_ahead = Json::requireInteger(rootobject, "ahead_by");
- auto commit_behind = Json::requireInteger(rootobject, "behind_by");
- result += QObject::tr("<p>The update removes %1 commits and adds the following %2:</p>").arg(commit_behind).arg(commit_ahead);
- print_commits();
- }
- result += QObject::tr("<p>You can <a href=\"%1\">look at the changes on github</a>.</p>").arg(diff_url);
- return result;
- }
- catch (const JSONValidationError &e)
- {
- qWarning() << "Got an unparseable commit log from github:" << e.what();
- qDebug() << json;
- }
- return QString();
-}
-
-void UpdateDialog::changelogLoaded()
-{
- QString result;
- switch(m_changelogType)
- {
- case CHANGELOG_COMMITS:
- result = reprocessCommits(changelogData);
- break;
- case CHANGELOG_MARKDOWN:
- result = reprocessMarkdown(changelogData);
- break;
- }
- changelogData.clear();
- ui->changelogBrowser->setHtml(result);
-}
-
-void UpdateDialog::changelogFailed(QString reason)
-{
- ui->changelogBrowser->setHtml(tr("<p align=\"center\" <span style=\"font-size:22pt;\">Failed to fetch changelog... Error: %1</span></p>").arg(reason));
-}
-
-void UpdateDialog::on_btnUpdateLater_clicked()
-{
- reject();
-}
-
-void UpdateDialog::on_btnUpdateNow_clicked()
-{
- done(UPDATE_NOW);
-}
-
-void UpdateDialog::closeEvent(QCloseEvent* evt)
-{
- APPLICATION->settings()->set("UpdateDialogGeometry", saveGeometry().toBase64());
- QDialog::closeEvent(evt);
-}
diff --git a/launcher/ui/dialogs/UpdateDialog.h b/launcher/ui/dialogs/UpdateDialog.h
deleted file mode 100644
index 07cbe09f..00000000
--- a/launcher/ui/dialogs/UpdateDialog.h
+++ /dev/null
@@ -1,67 +0,0 @@
-/* Copyright 2013-2021 MultiMC Contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#pragma once
-
-#include <QDialog>
-#include "net/NetJob.h"
-
-namespace Ui
-{
-class UpdateDialog;
-}
-
-enum UpdateAction
-{
- UPDATE_LATER = QDialog::Rejected,
- UPDATE_NOW = QDialog::Accepted,
-};
-
-enum ChangelogType
-{
- CHANGELOG_MARKDOWN,
- CHANGELOG_COMMITS
-};
-
-class UpdateDialog : public QDialog
-{
- Q_OBJECT
-
-public:
- explicit UpdateDialog(bool hasUpdate = true, QWidget *parent = 0);
- ~UpdateDialog();
-
-public slots:
- void on_btnUpdateNow_clicked();
- void on_btnUpdateLater_clicked();
-
- /// Starts loading the changelog
- void loadChangelog();
-
- /// Slot for when the chengelog loads successfully.
- void changelogLoaded();
-
- /// Slot for when the chengelog fails to load...
- void changelogFailed(QString reason);
-
-protected:
- void closeEvent(QCloseEvent * ) override;
-
-private:
- Ui::UpdateDialog *ui;
- QByteArray changelogData;
- NetJob::Ptr dljob;
- ChangelogType m_changelogType = CHANGELOG_MARKDOWN;
-};
diff --git a/launcher/ui/dialogs/UpdateDialog.ui b/launcher/ui/dialogs/UpdateDialog.ui
deleted file mode 100644
index 5eb9d88a..00000000
--- a/launcher/ui/dialogs/UpdateDialog.ui
+++ /dev/null
@@ -1,91 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<ui version="4.0">
- <class>UpdateDialog</class>
- <widget class="QDialog" name="UpdateDialog">
- <property name="geometry">
- <rect>
- <x>0</x>
- <y>0</y>
- <width>657</width>
- <height>673</height>
- </rect>
- </property>
- <property name="windowTitle">
- <string>Launcher Update</string>
- </property>
- <property name="windowIcon">
- <iconset>
- <normaloff>:/icons/toolbar/checkupdate</normaloff>:/icons/toolbar/checkupdate</iconset>
- </property>
- <layout class="QVBoxLayout" name="verticalLayout">
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout_2">
- <item>
- <widget class="QLabel" name="label">
- <property name="font">
- <font>
- <pointsize>14</pointsize>
- </font>
- </property>
- <property name="text">
- <string/>
- </property>
- <property name="alignment">
- <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
- </property>
- <property name="buddy">
- <cstring>changelogBrowser</cstring>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <widget class="QTextBrowser" name="changelogBrowser">
- <property name="openExternalLinks">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- <item>
- <layout class="QGridLayout" name="gridLayout">
- <item row="0" column="0">
- <widget class="QPushButton" name="btnUpdateNow">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="text">
- <string>Update now</string>
- </property>
- </widget>
- </item>
- <item row="0" column="1">
- <widget class="QPushButton" name="btnUpdateLater">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="text">
- <string>Don't update yet</string>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- </layout>
- </widget>
- <tabstops>
- <tabstop>changelogBrowser</tabstop>
- <tabstop>btnUpdateNow</tabstop>
- <tabstop>btnUpdateLater</tabstop>
- </tabstops>
- <resources>
- <include location="../../resources/multimc/multimc.qrc"/>
- </resources>
- <connections/>
-</ui>
diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp
index 69a8e3df..a9f44c0b 100644
--- a/launcher/ui/pages/global/LauncherPage.cpp
+++ b/launcher/ui/pages/global/LauncherPage.cpp
@@ -44,14 +44,13 @@
#include <QTextCharFormat>
#include <QMenuBar>
-#include "updater/UpdateChecker.h"
-
#include "settings/SettingsObject.h"
#include <FileSystem.h>
#include "Application.h"
#include "BuildConfig.h"
#include "DesktopServices.h"
#include "ui/themes/ITheme.h"
+#include "updater/ExternalUpdater.h"
#include <QApplication>
#include <QProcess>
@@ -80,32 +79,12 @@ LauncherPage::LauncherPage(QWidget *parent) : QWidget(parent), ui(new Ui::Launch
m_languageModel = APPLICATION->translations();
loadSettings();
- if(BuildConfig.UPDATER_ENABLED)
- {
- QObject::connect(APPLICATION->updateChecker().get(), &UpdateChecker::channelListLoaded, this, &LauncherPage::refreshUpdateChannelList);
+ ui->updateSettingsBox->setHidden(!APPLICATION->updater());
- if (APPLICATION->updateChecker()->hasChannels())
- {
- refreshUpdateChannelList();
- }
- else
- {
- APPLICATION->updateChecker()->updateChanList(false);
- }
-
- if (APPLICATION->updateChecker()->getExternalUpdater())
- {
- ui->updateChannelComboBox->setVisible(false);
- ui->updateChannelDescLabel->setVisible(false);
- ui->updateChannelLabel->setVisible(false);
- }
- }
- else
- {
- ui->updateSettingsBox->setHidden(true);
- }
connect(ui->fontSizeBox, SIGNAL(valueChanged(int)), SLOT(refreshFontPreview()));
connect(ui->consoleFont, SIGNAL(currentFontChanged(QFont)), SLOT(refreshFontPreview()));
+
+ connect(ui->themeCustomizationWidget, &ThemeCustomizationWidget::currentCatChanged, APPLICATION, &Application::currentCatChanged);
}
LauncherPage::~LauncherPage()
@@ -198,94 +177,15 @@ void LauncherPage::on_metadataDisableBtn_clicked()
ui->metadataWarningLabel->setHidden(!ui->metadataDisableBtn->isChecked());
}
-void LauncherPage::refreshUpdateChannelList()
-{
- // Stop listening for selection changes. It's going to change a lot while we update it and
- // we don't need to update the
- // description label constantly.
- QObject::disconnect(ui->updateChannelComboBox, SIGNAL(currentIndexChanged(int)), this,
- SLOT(updateChannelSelectionChanged(int)));
-
- QList<UpdateChecker::ChannelListEntry> channelList = APPLICATION->updateChecker()->getChannelList();
- ui->updateChannelComboBox->clear();
- int selection = -1;
- for (int i = 0; i < channelList.count(); i++)
- {
- UpdateChecker::ChannelListEntry entry = channelList.at(i);
-
- // When it comes to selection, we'll rely on the indexes of a channel entry being the
- // same in the
- // combo box as it is in the update checker's channel list.
- // This probably isn't very safe, but the channel list doesn't change often enough (or
- // at all) for
- // this to be a big deal. Hope it doesn't break...
- ui->updateChannelComboBox->addItem(entry.name);
-
- // If the update channel we just added was the selected one, set the current index in
- // the combo box to it.
- if (entry.id == m_currentUpdateChannel)
- {
- qDebug() << "Selected index" << i << "channel id" << m_currentUpdateChannel;
- selection = i;
- }
- }
-
- ui->updateChannelComboBox->setCurrentIndex(selection);
-
- // Start listening for selection changes again and update the description label.
- QObject::connect(ui->updateChannelComboBox, SIGNAL(currentIndexChanged(int)), this,
- SLOT(updateChannelSelectionChanged(int)));
- refreshUpdateChannelDesc();
-
- // Now that we've updated the channel list, we can enable the combo box.
- // It starts off disabled so that if the channel list hasn't been loaded, it will be
- // disabled.
- ui->updateChannelComboBox->setEnabled(true);
-}
-
-void LauncherPage::updateChannelSelectionChanged(int index)
-{
- refreshUpdateChannelDesc();
-}
-
-void LauncherPage::refreshUpdateChannelDesc()
-{
- // Get the channel list.
- QList<UpdateChecker::ChannelListEntry> channelList = APPLICATION->updateChecker()->getChannelList();
- int selectedIndex = ui->updateChannelComboBox->currentIndex();
- if (selectedIndex < 0)
- {
- return;
- }
- if (selectedIndex < channelList.count())
- {
- // Find the channel list entry with the given index.
- UpdateChecker::ChannelListEntry selected = channelList.at(selectedIndex);
-
- // Set the description text.
- ui->updateChannelDescLabel->setText(selected.description);
-
- // Set the currently selected channel ID.
- m_currentUpdateChannel = selected.id;
- }
-}
-
void LauncherPage::applySettings()
{
auto s = APPLICATION->settings();
// Updates
- if (BuildConfig.UPDATER_ENABLED && APPLICATION->updateChecker()->getExternalUpdater())
+ if (APPLICATION->updater())
{
- APPLICATION->updateChecker()->getExternalUpdater()->setAutomaticallyChecksForUpdates(
- ui->autoUpdateCheckBox->isChecked());
+ APPLICATION->updater()->setAutomaticallyChecksForUpdates(ui->autoUpdateCheckBox->isChecked());
}
- else
- {
- s->set("AutoUpdate", ui->autoUpdateCheckBox->isChecked());
- }
-
- s->set("UpdateChannel", m_currentUpdateChannel);
s->set("MenuBarInsteadOfToolBar", ui->preferMenuBarCheckBox->isChecked());
@@ -324,17 +224,11 @@ void LauncherPage::loadSettings()
{
auto s = APPLICATION->settings();
// Updates
- if (BuildConfig.UPDATER_ENABLED && APPLICATION->updateChecker()->getExternalUpdater())
- {
- ui->autoUpdateCheckBox->setChecked(
- APPLICATION->updateChecker()->getExternalUpdater()->getAutomaticallyChecksForUpdates());
- }
- else
+ if (APPLICATION->updater())
{
- ui->autoUpdateCheckBox->setChecked(s->get("AutoUpdate").toBool());
+ ui->autoUpdateCheckBox->setChecked(APPLICATION->updater()->getAutomaticallyChecksForUpdates());
}
- m_currentUpdateChannel = s->get("UpdateChannel").toString();
// Toolbar/menu bar settings (not applicable if native menu bar is present)
ui->toolsBox->setEnabled(!QMenuBar().isNativeMenuBar());
diff --git a/launcher/ui/pages/global/LauncherPage.h b/launcher/ui/pages/global/LauncherPage.h
index f38c922e..c60156c2 100644
--- a/launcher/ui/pages/global/LauncherPage.h
+++ b/launcher/ui/pages/global/LauncherPage.h
@@ -91,22 +91,10 @@ slots:
void on_metadataDisableBtn_clicked();
/*!
- * Updates the list of update channels in the combo box.
- */
- void refreshUpdateChannelList();
-
- /*!
- * Updates the channel description label.
- */
- void refreshUpdateChannelDesc();
-
- /*!
* Updates the font preview
*/
void refreshFontPreview();
- void updateChannelSelectionChanged(int index);
-
private:
Ui::LauncherPage *ui;
diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui
index 65f4a9d5..f084d970 100644
--- a/launcher/ui/pages/global/LauncherPage.ui
+++ b/launcher/ui/pages/global/LauncherPage.ui
@@ -58,33 +58,6 @@
</property>
</widget>
</item>
- <item>
- <widget class="QLabel" name="updateChannelLabel">
- <property name="text">
- <string>Up&amp;date Channel:</string>
- </property>
- <property name="buddy">
- <cstring>updateChannelComboBox</cstring>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QComboBox" name="updateChannelComboBox">
- <property name="enabled">
- <bool>false</bool>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QLabel" name="updateChannelDescLabel">
- <property name="text">
- <string>No channel selected.</string>
- </property>
- <property name="wordWrap">
- <bool>true</bool>
- </property>
- </widget>
- </item>
</layout>
</widget>
</item>
@@ -440,7 +413,6 @@
<tabstops>
<tabstop>tabWidget</tabstop>
<tabstop>autoUpdateCheckBox</tabstop>
- <tabstop>updateChannelComboBox</tabstop>
<tabstop>instDirTextBox</tabstop>
<tabstop>instDirBrowseBtn</tabstop>
<tabstop>modsDirTextBox</tabstop>
diff --git a/launcher/ui/pages/instance/LogPage.cpp b/launcher/ui/pages/instance/LogPage.cpp
index 8f9e569e..639cd711 100644
--- a/launcher/ui/pages/instance/LogPage.cpp
+++ b/launcher/ui/pages/instance/LogPage.cpp
@@ -40,7 +40,7 @@
#include "Application.h"
-#include <QIcon>
+#include <QIdentityProxyModel>
#include <QScrollBar>
#include <QShortcut>
diff --git a/launcher/ui/pages/instance/ManagedPackPage.cpp b/launcher/ui/pages/instance/ManagedPackPage.cpp
index 8d56d894..dc983d9a 100644
--- a/launcher/ui/pages/instance/ManagedPackPage.cpp
+++ b/launcher/ui/pages/instance/ManagedPackPage.cpp
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: 2022 flow <flowlnlnln@gmail.com>
+// SPDX-FileCopyrightText: 2022 flowln <flowlnlnln@gmail.com>
//
// SPDX-License-Identifier: GPL-3.0-only
diff --git a/launcher/ui/pages/instance/ManagedPackPage.h b/launcher/ui/pages/instance/ManagedPackPage.h
index d29a5e88..1ac6fc03 100644
--- a/launcher/ui/pages/instance/ManagedPackPage.h
+++ b/launcher/ui/pages/instance/ManagedPackPage.h
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: 2022 flow <flowlnlnln@gmail.com>
+// SPDX-FileCopyrightText: 2022 flowln <flowlnlnln@gmail.com>
//
// SPDX-License-Identifier: GPL-3.0-only
@@ -12,6 +12,8 @@
#include "modplatform/flame/FlameAPI.h"
#include "modplatform/flame/FlamePackIndex.h"
+#include "net/NetJob.h"
+
#include "ui/pages/BasePage.h"
#include <QWidget>
diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp
index 627e71e5..d9069915 100644
--- a/launcher/ui/pages/instance/ModFolderPage.cpp
+++ b/launcher/ui/pages/instance/ModFolderPage.cpp
@@ -49,8 +49,8 @@
#include "ui/GuiUtil.h"
#include "ui/dialogs/CustomMessageBox.h"
-#include "ui/dialogs/ModDownloadDialog.h"
#include "ui/dialogs/ModUpdateDialog.h"
+#include "ui/dialogs/ResourceDownloadDialog.h"
#include "DesktopServices.h"
@@ -59,7 +59,7 @@
#include "minecraft/mod/Mod.h"
#include "minecraft/mod/ModFolderModel.h"
-#include "modplatform/ModAPI.h"
+#include "modplatform/ResourceAPI.h"
#include "Version.h"
#include "tasks/ConcurrentTask.h"
@@ -153,12 +153,12 @@ void ModFolderPage::installMods()
return; // this is a null instance or a legacy instance
auto profile = static_cast<MinecraftInstance*>(m_instance)->getPackProfile();
- if (profile->getModLoaders() == ModAPI::Unspecified) {
+ if (!profile->getModLoaders().has_value()) {
QMessageBox::critical(this, tr("Error"), tr("Please install a mod loader first!"));
return;
}
- ModDownloadDialog mdownload(m_model, this, m_instance);
+ ResourceDownload::ModDownloadDialog mdownload(this, m_model, m_instance);
if (mdownload.exec()) {
ConcurrentTask* tasks = new ConcurrentTask(this);
connect(tasks, &Task::failed, [this, tasks](QString reason) {
diff --git a/launcher/ui/pages/instance/ResourcePackPage.h b/launcher/ui/pages/instance/ResourcePackPage.h
index 9633e3b4..db8af0c5 100644
--- a/launcher/ui/pages/instance/ResourcePackPage.h
+++ b/launcher/ui/pages/instance/ResourcePackPage.h
@@ -73,3 +73,4 @@ public:
return true;
}
};
+
diff --git a/launcher/ui/pages/instance/ScreenshotsPage.h b/launcher/ui/pages/instance/ScreenshotsPage.h
index 2eb0de04..89611b6d 100644
--- a/launcher/ui/pages/instance/ScreenshotsPage.h
+++ b/launcher/ui/pages/instance/ScreenshotsPage.h
@@ -44,6 +44,7 @@
class QFileSystemModel;
class QIdentityProxyModel;
+class QItemSelection;
namespace Ui
{
class ScreenshotsPage;
diff --git a/launcher/ui/pages/instance/ServersPage.cpp b/launcher/ui/pages/instance/ServersPage.cpp
index 6f8591a1..4b1fa08a 100644
--- a/launcher/ui/pages/instance/ServersPage.cpp
+++ b/launcher/ui/pages/instance/ServersPage.cpp
@@ -50,6 +50,7 @@
#include <QFileSystemWatcher>
#include <QMenu>
+#include <QTimer>
static const int COLUMN_COUNT = 2; // 3 , TBD: latency and other nice things.
diff --git a/launcher/ui/pages/instance/VersionPage.cpp b/launcher/ui/pages/instance/VersionPage.cpp
index d200652a..94315395 100644
--- a/launcher/ui/pages/instance/VersionPage.cpp
+++ b/launcher/ui/pages/instance/VersionPage.cpp
@@ -660,7 +660,7 @@ void VersionPage::onGameUpdateError(QString error)
CustomMessageBox::selectable(this, tr("Error updating instance"), error, QMessageBox::Warning)->show();
}
-Component * VersionPage::current()
+ComponentPtr VersionPage::current()
{
auto row = currentRow();
if(row < 0)
diff --git a/launcher/ui/pages/instance/VersionPage.h b/launcher/ui/pages/instance/VersionPage.h
index 166f36bb..183bad9a 100644
--- a/launcher/ui/pages/instance/VersionPage.h
+++ b/launcher/ui/pages/instance/VersionPage.h
@@ -99,7 +99,7 @@ private slots:
void updateVersionControls();
private:
- Component * current();
+ ComponentPtr current();
int currentRow();
void updateButtons(int row = -1);
void preselect(int row = 0);
diff --git a/launcher/ui/pages/instance/WorldListPage.cpp b/launcher/ui/pages/instance/WorldListPage.cpp
index 0020c461..d4a395d9 100644
--- a/launcher/ui/pages/instance/WorldListPage.cpp
+++ b/launcher/ui/pages/instance/WorldListPage.cpp
@@ -45,9 +45,9 @@
#include <QKeyEvent>
#include <QClipboard>
#include <QMessageBox>
+#include <QSortFilterProxyModel>
#include <QTreeView>
#include <QInputDialog>
-#include <QProcess>
#include <Qt>
#include "tools/MCEditTool.h"
diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp
index ed58eb32..3ffe6cb0 100644
--- a/launcher/ui/pages/modplatform/ModModel.cpp
+++ b/launcher/ui/pages/modplatform/ModModel.cpp
@@ -1,350 +1,70 @@
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
#include "ModModel.h"
-#include "BuildConfig.h"
-#include "Json.h"
-#include "ModPage.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
-#include "ui/dialogs/ModDownloadDialog.h"
-
-#include "ui/widgets/ProjectItem.h"
#include <QMessageBox>
-namespace ModPlatform {
-
-// HACK: We need this to prevent callbacks from calling the ListModel after it has already been deleted.
-// This leaks a tiny bit of memory per time the user has opened the mod dialog. How to make this better?
-static QHash<ListModel*, bool> s_running;
-
-ListModel::ListModel(ModPage* parent) : QAbstractListModel(parent), m_parent(parent) { s_running.insert(this, true); }
-
-ListModel::~ListModel()
-{
- s_running.find(this).value() = false;
-}
+namespace ResourceDownload {
-auto ListModel::debugName() const -> QString
-{
- return m_parent->debugName();
-}
+ModModel::ModModel(BaseInstance const& base_inst, ResourceAPI* api) : ResourceModel(api), m_base_instance(base_inst) {}
/******** Make data requests ********/
-void ListModel::fetchMore(const QModelIndex& parent)
-{
- if (parent.isValid())
- return;
- if (nextSearchOffset == 0) {
- qWarning() << "fetchMore with 0 offset is wrong...";
- return;
- }
- performPaginatedSearch();
-}
-
-auto ListModel::data(const QModelIndex& index, int role) const -> QVariant
-{
- int pos = index.row();
- if (pos >= modpacks.size() || pos < 0 || !index.isValid()) {
- return QString("INVALID INDEX %1").arg(pos);
- }
-
- ModPlatform::IndexedPack pack = modpacks.at(pos);
- switch (role) {
- case Qt::ToolTipRole: {
- if (pack.description.length() > 100) {
- // some magic to prevent to long tooltips and replace html linebreaks
- QString edit = pack.description.left(97);
- edit = edit.left(edit.lastIndexOf("<br>")).left(edit.lastIndexOf(" ")).append("...");
- return edit;
- }
- return pack.description;
- }
- case Qt::DecorationRole: {
- if (m_logoMap.contains(pack.logoName)) {
- return m_logoMap.value(pack.logoName);
- }
- QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder");
- // un-const-ify this
- ((ListModel*)this)->requestLogo(pack.logoName, pack.logoUrl);
- return icon;
- }
- case Qt::SizeHintRole:
- return QSize(0, 58);
- case Qt::UserRole: {
- QVariant v;
- v.setValue(pack);
- return v;
- }
- // Custom data
- case UserDataTypes::TITLE:
- return pack.name;
- case UserDataTypes::DESCRIPTION:
- return pack.description;
- case UserDataTypes::SELECTED:
- return m_parent->getDialog()->isModSelected(pack.name);
- default:
- break;
- }
-
- return {};
-}
-
-bool ListModel::setData(const QModelIndex &index, const QVariant &value, int role)
-{
- int pos = index.row();
- if (pos >= modpacks.size() || pos < 0 || !index.isValid())
- return false;
-
- modpacks[pos] = value.value<ModPlatform::IndexedPack>();
-
- return true;
-}
-
-void ListModel::requestModVersions(ModPlatform::IndexedPack const& current, QModelIndex index)
-{
- auto profile = (dynamic_cast<MinecraftInstance*>((dynamic_cast<ModPage*>(parent()))->m_instance))->getPackProfile();
-
- m_parent->apiProvider()->getVersions({ current.addonId.toString(), getMineVersions(), profile->getModLoaders() },
- [this, current, index](QJsonDocument& doc, QString addonId) {
- if (!s_running.constFind(this).value())
- return;
- versionRequestSucceeded(doc, addonId, index);
- });
-}
-
-void ListModel::performPaginatedSearch()
+ResourceAPI::SearchArgs ModModel::createSearchArguments()
{
- auto profile = (dynamic_cast<MinecraftInstance*>((dynamic_cast<ModPage*>(parent()))->m_instance))->getPackProfile();
-
- m_parent->apiProvider()->searchMods(
- this, { nextSearchOffset, currentSearchTerm, getSorts()[currentSort], profile->getModLoaders(), getMineVersions() });
-}
+ auto profile = static_cast<MinecraftInstance const&>(m_base_instance).getPackProfile();
-void ListModel::requestModInfo(ModPlatform::IndexedPack& current, QModelIndex index)
-{
- m_parent->apiProvider()->getModInfo(current, [this, index](QJsonDocument& doc, ModPlatform::IndexedPack& pack) {
- if (!s_running.constFind(this).value())
- return;
- infoRequestFinished(doc, pack, index);
- });
-}
+ Q_ASSERT(profile);
+ Q_ASSERT(m_filter);
-void ListModel::refresh()
-{
- if (jobPtr) {
- jobPtr->abort();
- searchState = ResetRequested;
- return;
- } else {
- beginResetModel();
- modpacks.clear();
- endResetModel();
- searchState = None;
- }
- nextSearchOffset = 0;
- performPaginatedSearch();
-}
+ std::optional<std::list<Version>> versions{};
-void ListModel::searchWithTerm(const QString& term, const int sort, const bool filter_changed)
-{
- if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort && !filter_changed) {
- return;
+ { // Version filter
+ if (!m_filter->versions.empty())
+ versions = m_filter->versions;
}
- currentSearchTerm = term;
- currentSort = sort;
-
- refresh();
-}
+ auto sort = getCurrentSortingMethodByIndex();
-void ListModel::getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback)
-{
- if (m_logoMap.contains(logo)) {
- callback(APPLICATION->metacache()
- ->resolveEntry(m_parent->metaEntryBase(), QString("logos/%1").arg(logo.section(".", 0, 0)))
- ->getFullPath());
- } else {
- requestLogo(logo, logoUrl);
- }
+ return { ModPlatform::ResourceType::MOD, m_next_search_offset, m_search_term, sort, profile->getModLoaders(), versions };
}
-void ListModel::requestLogo(QString logo, QString url)
+ResourceAPI::VersionSearchArgs ModModel::createVersionsArguments(QModelIndex& entry)
{
- if (m_loadingLogos.contains(logo) || m_failedLogos.contains(logo) || url.isEmpty()) {
- return;
- }
+ auto& pack = m_packs[entry.row()];
+ auto profile = static_cast<MinecraftInstance const&>(m_base_instance).getPackProfile();
- MetaEntryPtr entry =
- APPLICATION->metacache()->resolveEntry(m_parent->metaEntryBase(), QString("logos/%1").arg(logo.section(".", 0, 0)));
- auto job = new NetJob(QString("%1 Icon Download %2").arg(m_parent->debugName()).arg(logo), APPLICATION->network());
- job->addNetAction(Net::Download::makeCached(QUrl(url), entry));
+ Q_ASSERT(profile);
+ Q_ASSERT(m_filter);
- auto fullPath = entry->getFullPath();
- QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] {
- job->deleteLater();
- emit logoLoaded(logo, QIcon(fullPath));
- if (waitingCallbacks.contains(logo)) {
- waitingCallbacks.value(logo)(fullPath);
- }
- });
+ std::optional<std::list<Version>> versions{};
+ if (!m_filter->versions.empty())
+ versions = m_filter->versions;
- QObject::connect(job, &NetJob::failed, this, [this, logo, job] {
- job->deleteLater();
- emit logoFailed(logo);
- });
-
- job->start();
- m_loadingLogos.append(logo);
-}
-
-/******** Request callbacks ********/
-
-void ListModel::logoLoaded(QString logo, QIcon out)
-{
- m_loadingLogos.removeAll(logo);
- m_logoMap.insert(logo, out);
- for (int i = 0; i < modpacks.size(); i++) {
- if (modpacks[i].logoName == logo) {
- emit dataChanged(createIndex(i, 0), createIndex(i, 0), { Qt::DecorationRole });
- }
- }
+ return { pack, versions, profile->getModLoaders() };
}
-void ListModel::logoFailed(QString logo)
+ResourceAPI::ProjectInfoArgs ModModel::createInfoArguments(QModelIndex& entry)
{
- m_failedLogos.append(logo);
- m_loadingLogos.removeAll(logo);
+ auto& pack = m_packs[entry.row()];
+ return { pack };
}
-void ListModel::searchRequestFinished(QJsonDocument& doc)
+void ModModel::searchWithTerm(const QString& term, unsigned int sort, bool filter_changed)
{
- jobPtr.reset();
-
- QList<ModPlatform::IndexedPack> newList;
- auto packs = documentToArray(doc);
-
- for (auto packRaw : packs) {
- auto packObj = packRaw.toObject();
-
- ModPlatform::IndexedPack pack;
- try {
- loadIndexedPack(pack, packObj);
- newList.append(pack);
- } catch (const JSONValidationError& e) {
- qWarning() << "Error while loading mod from " << m_parent->debugName() << ": " << e.cause();
- continue;
- }
- }
-
- if (packs.size() < 25) {
- searchState = Finished;
- } else {
- nextSearchOffset += 25;
- searchState = CanPossiblyFetchMore;
- }
-
- // When you have a Qt build with assertions turned on, proceeding here will abort the application
- if (newList.size() == 0)
+ if (m_search_term == term && m_search_term.isNull() == term.isNull() && m_current_sort_index == sort && !filter_changed) {
return;
-
- beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1);
- modpacks.append(newList);
- endInsertRows();
-}
-
-void ListModel::searchRequestFailed(QString reason)
-{
- auto failed_action = jobPtr->getFailedActions().at(0);
- if (!failed_action->m_reply) {
- // Network error
- QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load mods."));
- } else if (failed_action->m_reply && failed_action->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 409) {
- // 409 Gone, notify user to update
- QMessageBox::critical(nullptr, tr("Error"),
- //: %1 refers to the launcher itself
- QString("%1 %2")
- .arg(m_parent->displayName())
- .arg(tr("API version too old!\nPlease update %1!").arg(BuildConfig.LAUNCHER_DISPLAYNAME)));
}
- jobPtr.reset();
- searchState = Finished;
-}
-
-void ListModel::searchRequestAborted()
-{
- if (searchState != ResetRequested)
- qCritical() << "Search task in ModModel aborted by an unknown reason!";
-
- // Retry fetching
- jobPtr.reset();
-
- beginResetModel();
- modpacks.clear();
- endResetModel();
-
- nextSearchOffset = 0;
- performPaginatedSearch();
-}
+ setSearchTerm(term);
+ m_current_sort_index = sort;
-void ListModel::infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index)
-{
- qDebug() << "Loading mod info";
-
- try {
- auto obj = Json::requireObject(doc);
- loadExtraPackInfo(pack, obj);
- } catch (const JSONValidationError& e) {
- qDebug() << doc;
- qWarning() << "Error while reading " << debugName() << " mod info: " << e.cause();
- }
-
- // Check if the index is still valid for this mod or not
- if (pack.addonId == data(index, Qt::UserRole).value<ModPlatform::IndexedPack>().addonId) {
- // Cache info :^)
- QVariant new_pack;
- new_pack.setValue(pack);
- if (!setData(index, new_pack, Qt::UserRole)) {
- qWarning() << "Failed to cache mod info!";
- }
- }
-
- m_parent->updateUi();
-}
-
-void ListModel::versionRequestSucceeded(QJsonDocument doc, QString addonId, const QModelIndex& index)
-{
- auto& current = m_parent->getCurrent();
- if (addonId != current.addonId) {
- return;
- }
-
- auto arr = doc.isObject() ? Json::ensureArray(doc.object(), "data") : doc.array();
-
- try {
- loadIndexedPackVersions(current, arr);
- } catch (const JSONValidationError& e) {
- qDebug() << doc;
- qWarning() << "Error while reading " << debugName() << " mod version: " << e.cause();
- }
-
- // Cache info :^)
- QVariant new_pack;
- new_pack.setValue(current);
- if (!setData(index, new_pack, Qt::UserRole)) {
- qWarning() << "Failed to cache mod versions!";
- }
-
-
- m_parent->updateModVersions();
+ refresh();
}
-} // namespace ModPlatform
-
-/******** Helpers ********/
-
-auto ModPlatform::ListModel::getMineVersions() const -> std::list<Version>
-{
- return m_parent->getFilter()->versions;
-}
+} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/ModModel.h b/launcher/ui/pages/modplatform/ModModel.h
index 36840649..5d4a7785 100644
--- a/launcher/ui/pages/modplatform/ModModel.h
+++ b/launcher/ui/pages/modplatform/ModModel.h
@@ -1,92 +1,52 @@
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
#pragma once
#include <QAbstractListModel>
+#include "BaseInstance.h"
+
#include "modplatform/ModIndex.h"
-#include "net/NetJob.h"
+#include "modplatform/ResourceAPI.h"
+
+#include "ui/pages/modplatform/ResourceModel.h"
+#include "ui/widgets/ModFilterWidget.h"
-class ModPage;
class Version;
-namespace ModPlatform {
+namespace ResourceDownload {
-using LogoMap = QMap<QString, QIcon>;
-using LogoCallback = std::function<void (QString)>;
+class ModPage;
-class ListModel : public QAbstractListModel {
+class ModModel : public ResourceModel {
Q_OBJECT
public:
- ListModel(ModPage* parent);
- ~ListModel() override;
-
- inline auto rowCount(const QModelIndex& parent) const -> int override { return parent.isValid() ? 0 : modpacks.size(); };
- inline auto columnCount(const QModelIndex& parent) const -> int override { return parent.isValid() ? 0 : 1; };
- inline auto flags(const QModelIndex& index) const -> Qt::ItemFlags override { return QAbstractListModel::flags(index); };
-
- auto debugName() const -> QString;
-
- /* Retrieve information from the model at a given index with the given role */
- auto data(const QModelIndex& index, int role) const -> QVariant override;
- bool setData(const QModelIndex &index, const QVariant &value, int role) override;
-
- inline void setActiveJob(NetJob::Ptr ptr) { jobPtr = ptr; }
- inline NetJob* activeJob() { return jobPtr.get(); }
+ ModModel(const BaseInstance&, ResourceAPI* api);
/* Ask the API for more information */
- void fetchMore(const QModelIndex& parent) override;
- void refresh();
- void searchWithTerm(const QString& term, const int sort, const bool filter_changed);
- void requestModInfo(ModPlatform::IndexedPack& current, QModelIndex index);
- void requestModVersions(const ModPlatform::IndexedPack& current, QModelIndex index);
+ void searchWithTerm(const QString& term, unsigned int sort, bool filter_changed);
- virtual void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) = 0;
- virtual void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) = 0;
- virtual void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) = 0;
+ void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override = 0;
+ void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override = 0;
+ void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override = 0;
- void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback);
-
- inline auto canFetchMore(const QModelIndex& parent) const -> bool override { return parent.isValid() ? false : searchState == CanPossiblyFetchMore; };
+ void setFilter(std::shared_ptr<ModFilterWidget::Filter> filter) { m_filter = filter; }
public slots:
- void searchRequestFinished(QJsonDocument& doc);
- void searchRequestFailed(QString reason);
- void searchRequestAborted();
-
- void infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index);
-
- void versionRequestSucceeded(QJsonDocument doc, QString addonId, const QModelIndex& index);
-
- protected slots:
-
- void logoFailed(QString logo);
- void logoLoaded(QString logo, QIcon out);
-
- void performPaginatedSearch();
+ ResourceAPI::SearchArgs createSearchArguments() override;
+ ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) override;
+ ResourceAPI::ProjectInfoArgs createInfoArguments(QModelIndex&) override;
protected:
- virtual auto documentToArray(QJsonDocument& obj) const -> QJsonArray = 0;
- virtual auto getSorts() const -> const char** = 0;
-
- void requestLogo(QString file, QString url);
-
- inline auto getMineVersions() const -> std::list<Version>;
+ auto documentToArray(QJsonDocument& obj) const -> QJsonArray override = 0;
protected:
- ModPage* m_parent;
-
- QList<ModPlatform::IndexedPack> modpacks;
-
- LogoMap m_logoMap;
- QMap<QString, LogoCallback> waitingCallbacks;
- QStringList m_failedLogos;
- QStringList m_loadingLogos;
+ const BaseInstance& m_base_instance;
- QString currentSearchTerm;
- int currentSort = 0;
- int nextSearchOffset = 0;
- enum SearchState { None, CanPossiblyFetchMore, ResetRequested, Finished } searchState = None;
-
- NetJob::Ptr jobPtr;
+ std::shared_ptr<ModFilterWidget::Filter> m_filter = nullptr;
};
-} // namespace ModPlatform
+
+} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp
index 0f30689e..04be43ad 100644
--- a/launcher/ui/pages/modplatform/ModPage.cpp
+++ b/launcher/ui/pages/modplatform/ModPage.cpp
@@ -1,4 +1,6 @@
-// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
@@ -35,59 +37,30 @@
*/
#include "ModPage.h"
-#include "Application.h"
-#include "ui_ModPage.h"
+#include "ui_ResourcePage.h"
#include <QDesktopServices>
#include <QKeyEvent>
#include <QRegularExpression>
+
#include <memory>
+#include "Application.h"
+#include "ResourceDownloadTask.h"
+
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
-#include "ui/dialogs/ModDownloadDialog.h"
-#include "ui/widgets/ProjectItem.h"
-#include "Markdown.h"
-
-ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api)
- : QWidget(dialog)
- , m_instance(instance)
- , ui(new Ui::ModPage)
- , dialog(dialog)
- , m_fetch_progress(this, false)
- , api(api)
-{
- ui->setupUi(this);
-
- connect(ui->searchButton, &QPushButton::clicked, this, &ModPage::triggerSearch);
- connect(ui->modFilterButton, &QPushButton::clicked, this, &ModPage::filterMods);
- connect(ui->packView, &QListView::doubleClicked, this, &ModPage::onModSelected);
- m_search_timer.setTimerType(Qt::TimerType::CoarseTimer);
- m_search_timer.setSingleShot(true);
+#include "ui/dialogs/ResourceDownloadDialog.h"
- connect(&m_search_timer, &QTimer::timeout, this, &ModPage::triggerSearch);
-
- ui->searchEdit->installEventFilter(this);
-
- ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
- ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300);
-
- m_fetch_progress.hideIfInactive(true);
- m_fetch_progress.setFixedHeight(24);
- m_fetch_progress.progressFormat("");
-
- ui->gridLayout_3->addWidget(&m_fetch_progress, 0, 0, 1, ui->gridLayout_3->columnCount());
-
- ui->packView->setItemDelegate(new ProjectItemDelegate(this));
- ui->packView->installEventFilter(this);
-
- connect(ui->packDescription, &QTextBrowser::anchorClicked, this, &ModPage::openUrl);
-}
+namespace ResourceDownload {
-ModPage::~ModPage()
+ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance& instance)
+ : ResourcePage(dialog, instance)
{
- delete ui;
+ connect(m_ui->searchButton, &QPushButton::clicked, this, &ModPage::triggerSearch);
+ connect(m_ui->resourceFilterButton, &QPushButton::clicked, this, &ModPage::filterMods);
+ connect(m_ui->packView, &QListView::doubleClicked, this, &ModPage::onResourceSelected);
}
void ModPage::setFilterWidget(unique_qobject_ptr<ModFilterWidget>& widget)
@@ -97,59 +70,19 @@ void ModPage::setFilterWidget(unique_qobject_ptr<ModFilterWidget>& widget)
m_filter_widget.swap(widget);
- ui->gridLayout_3->addWidget(m_filter_widget.get(), 0, 0, 1, ui->gridLayout_3->columnCount());
+ m_ui->gridLayout_3->addWidget(m_filter_widget.get(), 0, 0, 1, m_ui->gridLayout_3->columnCount());
- m_filter_widget->setInstance(static_cast<MinecraftInstance*>(m_instance));
+ m_filter_widget->setInstance(&static_cast<MinecraftInstance&>(m_base_instance));
m_filter = m_filter_widget->getFilter();
connect(m_filter_widget.get(), &ModFilterWidget::filterChanged, this, [&]{
- ui->searchButton->setStyleSheet("text-decoration: underline");
+ m_ui->searchButton->setStyleSheet("text-decoration: underline");
});
connect(m_filter_widget.get(), &ModFilterWidget::filterUnchanged, this, [&]{
- ui->searchButton->setStyleSheet("text-decoration: none");
+ m_ui->searchButton->setStyleSheet("text-decoration: none");
});
}
-
-/******** Qt things ********/
-
-void ModPage::openedImpl()
-{
- updateSelectionButton();
- triggerSearch();
-}
-
-auto ModPage::eventFilter(QObject* watched, QEvent* event) -> bool
-{
- if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) {
- auto* keyEvent = dynamic_cast<QKeyEvent*>(event);
- if (keyEvent->key() == Qt::Key_Return) {
- triggerSearch();
- keyEvent->accept();
- return true;
- } else {
- if (m_search_timer.isActive())
- m_search_timer.stop();
-
- m_search_timer.start(350);
- }
- } else if (watched == ui->packView && event->type() == QEvent::KeyPress) {
- auto* keyEvent = dynamic_cast<QKeyEvent*>(event);
- if (keyEvent->key() == Qt::Key_Return) {
- onModSelected();
-
- // To have the 'select mod' button outlined instead of the 'review and confirm' one
- ui->modSelectionButton->setFocus(Qt::FocusReason::ShortcutFocusReason);
- ui->packView->setFocus(Qt::FocusReason::NoFocusReason);
-
- keyEvent->accept();
- return true;
- }
- }
- return QWidget::eventFilter(watched, event);
-}
-
-
/******** Callbacks to events in the UI (set up in the derived classes) ********/
void ModPage::filterMods()
@@ -163,176 +96,37 @@ void ModPage::triggerSearch()
m_filter = m_filter_widget->getFilter();
if (changed) {
- ui->packView->clearSelection();
- ui->packDescription->clear();
- ui->versionSelectionBox->clear();
- updateSelectionButton();
- }
-
- listModel->searchWithTerm(getSearchTerm(), ui->sortByBox->currentIndex(), changed);
- m_fetch_progress.watch(listModel->activeJob());
-}
-
-QString ModPage::getSearchTerm() const
-{
- return ui->searchEdit->text();
-}
-void ModPage::setSearchTerm(QString term)
-{
- ui->searchEdit->setText(term);
-}
-
-void ModPage::onSelectionChanged(QModelIndex curr, QModelIndex prev)
-{
- ui->versionSelectionBox->clear();
-
- if (!curr.isValid()) { return; }
-
- current = listModel->data(curr, Qt::UserRole).value<ModPlatform::IndexedPack>();
-
- if (!current.versionsLoaded) {
- qDebug() << QString("Loading %1 mod versions").arg(debugName());
-
- ui->modSelectionButton->setText(tr("Loading versions..."));
- ui->modSelectionButton->setEnabled(false);
-
- listModel->requestModVersions(current, curr);
- } else {
- for (int i = 0; i < current.versions.size(); i++) {
- ui->versionSelectionBox->addItem(current.versions[i].version, QVariant(i));
- }
- if (ui->versionSelectionBox->count() == 0) { ui->versionSelectionBox->addItem(tr("No valid version found."), QVariant(-1)); }
-
+ m_ui->packView->clearSelection();
+ m_ui->packDescription->clear();
+ m_ui->versionSelectionBox->clear();
updateSelectionButton();
}
- if(!current.extraDataLoaded){
- qDebug() << QString("Loading %1 mod info").arg(debugName());
-
- listModel->requestModInfo(current, curr);
- }
-
- updateUi();
-}
-
-void ModPage::onVersionSelectionChanged(QString data)
-{
- if (data.isNull() || data.isEmpty()) {
- selectedVersion = -1;
- return;
- }
- selectedVersion = ui->versionSelectionBox->currentData().toInt();
- updateSelectionButton();
-}
-
-void ModPage::onModSelected()
-{
- if (selectedVersion < 0)
- return;
-
- auto& version = current.versions[selectedVersion];
- if (dialog->isModSelected(current.name, version.fileName)) {
- dialog->removeSelectedMod(current.name);
- } else {
- bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool();
- dialog->addSelectedMod(current.name, new ModDownloadTask(current, version, dialog->mods, is_indexed));
- }
-
- updateSelectionButton();
-
- /* Force redraw on the mods list when the selection changes */
- ui->packView->adjustSize();
+ static_cast<ModModel*>(m_model)->searchWithTerm(getSearchTerm(), m_ui->sortByBox->currentData().toUInt(), changed);
+ m_fetch_progress.watch(m_model->activeSearchJob().get());
}
-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)
+QMap<QString, QString> ModPage::urlHandlers() const
{
- // 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;
- QString 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 (!page.isNull()) {
- 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);
+ QMap<QString, QString> map;
+ map.insert(QRegularExpression::anchoredPattern("(?:www\\.)?modrinth\\.com\\/mod\\/([^\\/]+)\\/?"), "modrinth");
+ map.insert(QRegularExpression::anchoredPattern("(?:www\\.)?curseforge\\.com\\/minecraft\\/mc-mods\\/([^\\/]+)\\/?"), "curseforge");
+ map.insert(QRegularExpression::anchoredPattern("minecraft\\.curseforge\\.com\\/projects\\/([^\\/]+)\\/?"), "curseforge");
+ return map;
}
/******** Make changes to the UI ********/
-void ModPage::retranslate()
-{
- ui->retranslateUi(this);
-}
-
-void ModPage::updateModVersions(int prev_count)
+void ModPage::updateVersionList()
{
- auto packProfile = (dynamic_cast<MinecraftInstance*>(m_instance))->getPackProfile();
+ m_ui->versionSelectionBox->clear();
+ auto packProfile = (dynamic_cast<MinecraftInstance&>(m_base_instance)).getPackProfile();
QString mcVersion = packProfile->getComponentVersion("net.minecraft");
- for (int i = 0; i < current.versions.size(); i++) {
- auto version = current.versions[i];
+ auto current_pack = getCurrentPack();
+ for (int i = 0; i < current_pack.versions.size(); i++) {
+ auto version = current_pack.versions[i];
bool valid = false;
for(auto& mcVer : m_filter->versions){
//NOTE: Flame doesn't care about loader, so passing it changes nothing.
@@ -344,87 +138,20 @@ void ModPage::updateModVersions(int prev_count)
// Only add the version if it's valid or using the 'Any' filter, but never if the version is opted out
if ((valid || m_filter->versions.empty()) && !optedOut(version))
- ui->versionSelectionBox->addItem(version.version, QVariant(i));
+ m_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));
- ui->modSelectionButton->setText(tr("Cannot select invalid version :("));
+ if (m_ui->versionSelectionBox->count() == 0) {
+ m_ui->versionSelectionBox->addItem(tr("No valid version found!"), QVariant(-1));
+ m_ui->resourceSelectionButton->setText(tr("Cannot select invalid version :("));
}
updateSelectionButton();
}
-
-void ModPage::updateSelectionButton()
+void ModPage::addResourceToDialog(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& version)
{
- if (!isOpened || selectedVersion < 0) {
- ui->modSelectionButton->setEnabled(false);
- return;
- }
-
- ui->modSelectionButton->setEnabled(true);
- auto& version = current.versions[selectedVersion];
- if (!dialog->isModSelected(current.name, version.fileName)) {
- ui->modSelectionButton->setText(tr("Select mod for download"));
- } else {
- ui->modSelectionButton->setText(tr("Deselect mod for download"));
- }
+ bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool();
+ m_parent_dialog->addResource(pack, version, is_indexed);
}
-void ModPage::updateUi()
-{
- QString text = "";
- QString name = current.name;
-
- if (current.websiteUrl.isEmpty())
- text = name;
- else
- text = "<a href=\"" + current.websiteUrl + "\">" + name + "</a>";
-
- if (!current.authors.empty()) {
- auto authorToStr = [](ModPlatform::ModpackAuthor& author) -> QString {
- if (author.url.isEmpty()) { return author.name; }
- return QString("<a href=\"%1\">%2</a>").arg(author.url, author.name);
- };
- QStringList authorStrs;
- for (auto& author : current.authors) {
- authorStrs.push_back(authorToStr(author));
- }
- text += "<br>" + tr(" by ") + authorStrs.join(", ");
- }
-
- if (current.extraDataLoaded) {
- if (!current.extraData.donate.isEmpty()) {
- text += "<br><br>" + tr("Donate information: ");
- auto donateToStr = [](ModPlatform::DonationData& donate) -> QString {
- return QString("<a href=\"%1\">%2</a>").arg(donate.url, donate.platform);
- };
- QStringList donates;
- for (auto& donate : current.extraData.donate) {
- donates.append(donateToStr(donate));
- }
- text += donates.join(", ");
- }
-
- if (!current.extraData.issuesUrl.isEmpty()
- || !current.extraData.sourceUrl.isEmpty()
- || !current.extraData.wikiUrl.isEmpty()
- || !current.extraData.discordUrl.isEmpty()) {
- text += "<br><br>" + tr("External links:") + "<br>";
- }
-
- if (!current.extraData.issuesUrl.isEmpty())
- text += "- " + tr("Issues: <a href=%1>%1</a>").arg(current.extraData.issuesUrl) + "<br>";
- if (!current.extraData.wikiUrl.isEmpty())
- text += "- " + tr("Wiki: <a href=%1>%1</a>").arg(current.extraData.wikiUrl) + "<br>";
- if (!current.extraData.sourceUrl.isEmpty())
- text += "- " + tr("Source code: <a href=%1>%1</a>").arg(current.extraData.sourceUrl) + "<br>";
- if (!current.extraData.discordUrl.isEmpty())
- text += "- " + tr("Discord: <a href=%1>%1</a>").arg(current.extraData.discordUrl) + "<br>";
- }
-
- text += "<hr>";
-
- ui->packDescription->setHtml(text + (current.extraData.body.isEmpty() ? current.description : markdownToHTML(current.extraData.body)));
- ui->packDescription->flush();
-}
+} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/ModPage.h b/launcher/ui/pages/modplatform/ModPage.h
index c9ccbaf2..c3b58cd6 100644
--- a/launcher/ui/pages/modplatform/ModPage.h
+++ b/launcher/ui/pages/modplatform/ModPage.h
@@ -1,105 +1,76 @@
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
#pragma once
#include <QWidget>
-#include "Application.h"
-#include "modplatform/ModAPI.h"
#include "modplatform/ModIndex.h"
-#include "ui/pages/BasePage.h"
+
+#include "ui/pages/modplatform/ResourcePage.h"
#include "ui/pages/modplatform/ModModel.h"
#include "ui/widgets/ModFilterWidget.h"
-#include "ui/widgets/ProgressWidget.h"
-
-class ModDownloadDialog;
namespace Ui {
-class ModPage;
+class ResourcePage;
}
+namespace ResourceDownload {
+
+class ModDownloadDialog;
+
/* This page handles most logic related to browsing and selecting mods to download. */
-class ModPage : public QWidget, public BasePage {
+class ModPage : public ResourcePage {
Q_OBJECT
public:
template<typename T>
- static T* create(ModDownloadDialog* dialog, BaseInstance* instance)
+ static T* create(ModDownloadDialog* dialog, BaseInstance& instance)
{
auto page = new T(dialog, instance);
+ auto model = static_cast<ModModel*>(page->getModel());
- auto filter_widget = ModFilterWidget::create(static_cast<MinecraftInstance*>(instance)->getPackProfile()->getComponentVersion("net.minecraft"), page);
+ auto filter_widget = ModFilterWidget::create(static_cast<MinecraftInstance&>(instance).getPackProfile()->getComponentVersion("net.minecraft"), page);
page->setFilterWidget(filter_widget);
+ model->setFilter(page->getFilter());
+
+ connect(model, &ResourceModel::versionListUpdated, page, &ResourcePage::updateVersionList);
+ connect(model, &ResourceModel::projectInfoUpdated, page, &ResourcePage::updateUi);
return page;
}
- ~ModPage() override;
-
- /* Affects what the user sees */
- auto displayName() const -> QString override = 0;
- auto icon() const -> QIcon override = 0;
- auto id() const -> QString override = 0;
- auto helpPage() const -> QString override = 0;
+ ~ModPage() override = default;
- /* Used internally */
- virtual auto metaEntryBase() const -> QString = 0;
- virtual auto debugName() const -> QString = 0;
+ //: The plural version of 'mod'
+ [[nodiscard]] inline QString resourcesString() const override { return tr("mods"); }
+ //: The singular version of 'mods'
+ [[nodiscard]] inline QString resourceString() const override { return tr("mod"); }
+ [[nodiscard]] QMap<QString, QString> urlHandlers() const override;
- void retranslate() override;
+ void addResourceToDialog(ModPlatform::IndexedPack&, ModPlatform::IndexedVersion&) override;
- void updateUi();
+ virtual auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, std::optional<ResourceAPI::ModLoaderTypes> loaders = {}) const -> bool = 0;
- auto shouldDisplay() const -> bool override = 0;
- virtual auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders = ModAPI::Unspecified) const -> bool = 0;
- virtual bool optedOut(ModPlatform::IndexedVersion& ver) const { return false; };
-
- auto apiProvider() -> ModAPI* { return api.get(); };
+ [[nodiscard]] bool supportsFiltering() const override { return true; };
auto getFilter() const -> const std::shared_ptr<ModFilterWidget::Filter> { return m_filter; }
- auto getDialog() const -> const ModDownloadDialog* { return dialog; }
-
- /** Get the current term in the search bar. */
- auto getSearchTerm() const -> QString;
- /** Programatically set the term in the search bar. */
- void setSearchTerm(QString);
-
void setFilterWidget(unique_qobject_ptr<ModFilterWidget>&);
- auto getCurrent() -> ModPlatform::IndexedPack& { return current; }
- void updateModVersions(int prev_count = -1);
-
- void openedImpl() override;
- auto eventFilter(QObject* watched, QEvent* event) -> bool override;
-
- BaseInstance* m_instance;
+ public slots:
+ void updateVersionList() override;
protected:
- ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api);
- void updateSelectionButton();
+ ModPage(ModDownloadDialog* dialog, BaseInstance& instance);
protected slots:
virtual void filterMods();
- void triggerSearch();
- void onSelectionChanged(QModelIndex first, QModelIndex second);
- void onVersionSelectionChanged(QString data);
- void onModSelected();
- virtual void openUrl(const QUrl& url);
+ void triggerSearch() override;
protected:
- Ui::ModPage* ui = nullptr;
- ModDownloadDialog* dialog = nullptr;
-
unique_qobject_ptr<ModFilterWidget> m_filter_widget;
std::shared_ptr<ModFilterWidget::Filter> m_filter;
-
- ProgressWidget m_fetch_progress;
-
- ModPlatform::ListModel* listModel = nullptr;
- ModPlatform::IndexedPack current;
-
- std::unique_ptr<ModAPI> api;
-
- int selectedVersion = -1;
-
- // Used to do instant searching with a delay to cache quick changes
- QTimer m_search_timer;
};
+
+} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/ResourceModel.cpp b/launcher/ui/pages/modplatform/ResourceModel.cpp
new file mode 100644
index 00000000..db7d26f8
--- /dev/null
+++ b/launcher/ui/pages/modplatform/ResourceModel.cpp
@@ -0,0 +1,444 @@
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
+#include "ResourceModel.h"
+
+#include <QCryptographicHash>
+#include <QIcon>
+#include <QMessageBox>
+#include <QPixmapCache>
+#include <QUrl>
+
+#include "Application.h"
+#include "BuildConfig.h"
+#include "Json.h"
+
+#include "net/Download.h"
+#include "net/NetJob.h"
+
+#include "modplatform/ModIndex.h"
+
+#include "ui/widgets/ProjectItem.h"
+
+namespace ResourceDownload {
+
+QHash<ResourceModel*, bool> ResourceModel::s_running_models;
+
+ResourceModel::ResourceModel(ResourceAPI* api) : QAbstractListModel(), m_api(api)
+{
+ s_running_models.insert(this, true);
+}
+
+ResourceModel::~ResourceModel()
+{
+ s_running_models.find(this).value() = false;
+}
+
+auto ResourceModel::data(const QModelIndex& index, int role) const -> QVariant
+{
+ int pos = index.row();
+ if (pos >= m_packs.size() || pos < 0 || !index.isValid()) {
+ return QString("INVALID INDEX %1").arg(pos);
+ }
+
+ auto pack = m_packs.at(pos);
+ switch (role) {
+ case Qt::ToolTipRole: {
+ if (pack.description.length() > 100) {
+ // some magic to prevent to long tooltips and replace html linebreaks
+ QString edit = pack.description.left(97);
+ edit = edit.left(edit.lastIndexOf("<br>")).left(edit.lastIndexOf(" ")).append("...");
+ return edit;
+ }
+ return pack.description;
+ }
+ case Qt::DecorationRole: {
+ if (auto icon_or_none = const_cast<ResourceModel*>(this)->getIcon(const_cast<QModelIndex&>(index), pack.logoUrl);
+ icon_or_none.has_value())
+ return icon_or_none.value();
+
+ return APPLICATION->getThemedIcon("screenshot-placeholder");
+ }
+ case Qt::SizeHintRole:
+ return QSize(0, 58);
+ case Qt::UserRole: {
+ QVariant v;
+ v.setValue(pack);
+ return v;
+ }
+ // Custom data
+ case UserDataTypes::TITLE:
+ return pack.name;
+ case UserDataTypes::DESCRIPTION:
+ return pack.description;
+ case UserDataTypes::SELECTED:
+ return pack.isAnyVersionSelected();
+ default:
+ break;
+ }
+
+ return {};
+}
+
+QHash<int, QByteArray> ResourceModel::roleNames() const
+{
+ QHash<int, QByteArray> roles;
+
+ roles[Qt::ToolTipRole] = "toolTip";
+ roles[Qt::DecorationRole] = "decoration";
+ roles[Qt::SizeHintRole] = "sizeHint";
+ roles[Qt::UserRole] = "pack";
+ roles[UserDataTypes::TITLE] = "title";
+ roles[UserDataTypes::DESCRIPTION] = "description";
+ roles[UserDataTypes::SELECTED] = "selected";
+
+ return roles;
+}
+
+bool ResourceModel::setData(const QModelIndex& index, const QVariant& value, int role)
+{
+ int pos = index.row();
+ if (pos >= m_packs.size() || pos < 0 || !index.isValid())
+ return false;
+
+ m_packs[pos] = value.value<ModPlatform::IndexedPack>();
+ emit dataChanged(index, index);
+
+ return true;
+}
+
+QString ResourceModel::debugName() const
+{
+ return "ResourceDownload (Model)";
+}
+
+void ResourceModel::fetchMore(const QModelIndex& parent)
+{
+ if (parent.isValid() || m_search_state == SearchState::Finished)
+ return;
+
+ search();
+}
+
+void ResourceModel::search()
+{
+ if (hasActiveSearchJob())
+ return;
+
+ auto args{ createSearchArguments() };
+
+ auto callbacks{ createSearchCallbacks() };
+
+ // Use defaults if no callbacks are set
+ if (!callbacks.on_succeed)
+ callbacks.on_succeed = [this](auto& doc) {
+ if (!s_running_models.constFind(this).value())
+ return;
+ searchRequestSucceeded(doc);
+ };
+ if (!callbacks.on_fail)
+ callbacks.on_fail = [this](QString reason, int network_error_code) {
+ if (!s_running_models.constFind(this).value())
+ return;
+ searchRequestFailed(reason, network_error_code);
+ };
+ if (!callbacks.on_abort)
+ callbacks.on_abort = [this] {
+ if (!s_running_models.constFind(this).value())
+ return;
+ searchRequestAborted();
+ };
+
+ if (auto job = m_api->searchProjects(std::move(args), std::move(callbacks)); job)
+ runSearchJob(job);
+}
+
+void ResourceModel::loadEntry(QModelIndex& entry)
+{
+ auto const& pack = m_packs[entry.row()];
+
+ if (!hasActiveInfoJob())
+ m_current_info_job.clear();
+
+ if (!pack.versionsLoaded) {
+ auto args{ createVersionsArguments(entry) };
+ auto callbacks{ createVersionsCallbacks(entry) };
+
+ // Use default if no callbacks are set
+ if (!callbacks.on_succeed)
+ callbacks.on_succeed = [this, entry](auto& doc, auto pack) {
+ if (!s_running_models.constFind(this).value())
+ return;
+ versionRequestSucceeded(doc, pack, entry);
+ };
+
+ if (auto job = m_api->getProjectVersions(std::move(args), std::move(callbacks)); job)
+ runInfoJob(job);
+ }
+
+ if (!pack.extraDataLoaded) {
+ auto args{ createInfoArguments(entry) };
+ auto callbacks{ createInfoCallbacks(entry) };
+
+ // Use default if no callbacks are set
+ if (!callbacks.on_succeed)
+ callbacks.on_succeed = [this, entry](auto& doc, auto pack) {
+ if (!s_running_models.constFind(this).value())
+ return;
+ infoRequestSucceeded(doc, pack, entry);
+ };
+
+ if (auto job = m_api->getProjectInfo(std::move(args), std::move(callbacks)); job)
+ runInfoJob(job);
+ }
+}
+
+void ResourceModel::refresh()
+{
+ bool reset_requested = false;
+
+ if (hasActiveInfoJob()) {
+ m_current_info_job.abort();
+ reset_requested = true;
+ }
+
+ if (hasActiveSearchJob()) {
+ m_current_search_job->abort();
+ reset_requested = true;
+ }
+
+ if (reset_requested) {
+ m_search_state = SearchState::ResetRequested;
+ return;
+ }
+
+ clearData();
+ m_search_state = SearchState::None;
+
+ m_next_search_offset = 0;
+ search();
+}
+
+void ResourceModel::clearData()
+{
+ beginResetModel();
+ m_packs.clear();
+ endResetModel();
+}
+
+void ResourceModel::runSearchJob(Task::Ptr ptr)
+{
+ m_current_search_job = ptr;
+ m_current_search_job->start();
+}
+void ResourceModel::runInfoJob(Task::Ptr ptr)
+{
+ if (!m_current_info_job.isRunning())
+ m_current_info_job.clear();
+
+ m_current_info_job.addTask(ptr);
+
+ if (!m_current_info_job.isRunning())
+ m_current_info_job.run();
+}
+
+std::optional<ResourceAPI::SortingMethod> ResourceModel::getCurrentSortingMethodByIndex() const
+{
+ std::optional<ResourceAPI::SortingMethod> sort{};
+
+ { // Find sorting method by ID
+ auto sorting_methods = getSortingMethods();
+ auto method = std::find_if(sorting_methods.constBegin(), sorting_methods.constEnd(),
+ [this](auto const& e) { return m_current_sort_index == e.index; });
+ if (method != sorting_methods.constEnd())
+ sort = *method;
+ }
+
+ return sort;
+}
+
+std::optional<QIcon> ResourceModel::getIcon(QModelIndex& index, const QUrl& url)
+{
+ QPixmap pixmap;
+ if (QPixmapCache::find(url.toString(), &pixmap))
+ return { pixmap };
+
+ if (!m_current_icon_job)
+ m_current_icon_job.reset(new NetJob("IconJob", APPLICATION->network()));
+
+ if (m_currently_running_icon_actions.contains(url))
+ return {};
+ if (m_failed_icon_actions.contains(url))
+ return {};
+
+ auto cache_entry = APPLICATION->metacache()->resolveEntry(
+ metaEntryBase(),
+ QString("logos/%1").arg(QString(QCryptographicHash::hash(url.toEncoded(), QCryptographicHash::Algorithm::Sha1).toHex())));
+ auto icon_fetch_action = Net::Download::makeCached(url, cache_entry);
+
+ auto full_file_path = cache_entry->getFullPath();
+ connect(icon_fetch_action.get(), &NetAction::succeeded, this, [=] {
+ auto icon = QIcon(full_file_path);
+ QPixmapCache::insert(url.toString(), icon.pixmap(icon.actualSize({ 64, 64 })));
+
+ m_currently_running_icon_actions.remove(url);
+
+ emit dataChanged(index, index, { Qt::DecorationRole });
+ });
+ connect(icon_fetch_action.get(), &NetAction::failed, this, [=] {
+ m_currently_running_icon_actions.remove(url);
+ m_failed_icon_actions.insert(url);
+ });
+
+ m_currently_running_icon_actions.insert(url);
+
+ m_current_icon_job->addNetAction(icon_fetch_action);
+ if (!m_current_icon_job->isRunning())
+ QMetaObject::invokeMethod(m_current_icon_job.get(), &NetJob::start);
+
+ return {};
+}
+
+// No 'forgor to implement' shall pass here :blobfox_knife:
+#define NEED_FOR_CALLBACK_ASSERT(name) \
+ Q_ASSERT_X(0 != 0, #name, "You NEED to re-implement this if you intend on using the default callbacks.")
+
+QJsonArray ResourceModel::documentToArray(QJsonDocument& doc) const
+{
+ NEED_FOR_CALLBACK_ASSERT("documentToArray");
+ return {};
+}
+void ResourceModel::loadIndexedPack(ModPlatform::IndexedPack&, QJsonObject&)
+{
+ NEED_FOR_CALLBACK_ASSERT("loadIndexedPack");
+}
+void ResourceModel::loadExtraPackInfo(ModPlatform::IndexedPack&, QJsonObject&)
+{
+ NEED_FOR_CALLBACK_ASSERT("loadExtraPackInfo");
+}
+void ResourceModel::loadIndexedPackVersions(ModPlatform::IndexedPack&, QJsonArray&)
+{
+ NEED_FOR_CALLBACK_ASSERT("loadIndexedPackVersions");
+}
+
+/* Default callbacks */
+
+void ResourceModel::searchRequestSucceeded(QJsonDocument& doc)
+{
+ QList<ModPlatform::IndexedPack> newList;
+ auto packs = documentToArray(doc);
+
+ for (auto packRaw : packs) {
+ auto packObj = packRaw.toObject();
+
+ ModPlatform::IndexedPack pack;
+ try {
+ loadIndexedPack(pack, packObj);
+ newList.append(pack);
+ } catch (const JSONValidationError& e) {
+ qWarning() << "Error while loading resource from " << debugName() << ": " << e.cause();
+ continue;
+ }
+ }
+
+ if (packs.size() < 25) {
+ m_search_state = SearchState::Finished;
+ } else {
+ m_next_search_offset += 25;
+ m_search_state = SearchState::CanFetchMore;
+ }
+
+ // When you have a Qt build with assertions turned on, proceeding here will abort the application
+ if (newList.size() == 0)
+ return;
+
+ beginInsertRows(QModelIndex(), m_packs.size(), m_packs.size() + newList.size() - 1);
+ m_packs.append(newList);
+ endInsertRows();
+}
+
+void ResourceModel::searchRequestFailed(QString reason, int network_error_code)
+{
+ switch (network_error_code) {
+ default:
+ // Network error
+ QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load mods."));
+ break;
+ case 409:
+ // 409 Gone, notify user to update
+ QMessageBox::critical(nullptr, tr("Error"),
+ QString("%1").arg(tr("API version too old!\nPlease update %1!").arg(BuildConfig.LAUNCHER_DISPLAYNAME)));
+ break;
+ }
+
+ m_search_state = SearchState::Finished;
+}
+
+void ResourceModel::searchRequestAborted()
+{
+ if (m_search_state != SearchState::ResetRequested)
+ qCritical() << "Search task in" << debugName() << "aborted by an unknown reason!";
+
+ // Retry fetching
+ clearData();
+
+ m_next_search_offset = 0;
+ search();
+}
+
+void ResourceModel::versionRequestSucceeded(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index)
+{
+ auto current_pack = data(index, Qt::UserRole).value<ModPlatform::IndexedPack>();
+
+ // Check if the index is still valid for this resource or not
+ if (pack.addonId != current_pack.addonId)
+ return;
+
+ try {
+ auto arr = doc.isObject() ? Json::ensureArray(doc.object(), "data") : doc.array();
+ loadIndexedPackVersions(current_pack, arr);
+ } catch (const JSONValidationError& e) {
+ qDebug() << doc;
+ qWarning() << "Error while reading " << debugName() << " resource version: " << e.cause();
+ }
+
+ // Cache info :^)
+ QVariant new_pack;
+ new_pack.setValue(current_pack);
+ if (!setData(index, new_pack, Qt::UserRole)) {
+ qWarning() << "Failed to cache resource versions!";
+ return;
+ }
+
+ emit versionListUpdated();
+}
+
+void ResourceModel::infoRequestSucceeded(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index)
+{
+ auto current_pack = data(index, Qt::UserRole).value<ModPlatform::IndexedPack>();
+
+ // Check if the index is still valid for this resource or not
+ if (pack.addonId != current_pack.addonId)
+ return;
+
+ try {
+ auto obj = Json::requireObject(doc);
+ loadExtraPackInfo(current_pack, obj);
+ } catch (const JSONValidationError& e) {
+ qDebug() << doc;
+ qWarning() << "Error while reading " << debugName() << " resource info: " << e.cause();
+ }
+
+ // Cache info :^)
+ QVariant new_pack;
+ new_pack.setValue(current_pack);
+ if (!setData(index, new_pack, Qt::UserRole)) {
+ qWarning() << "Failed to cache resource info!";
+ return;
+ }
+
+ emit projectInfoUpdated();
+}
+
+} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/ResourceModel.h b/launcher/ui/pages/modplatform/ResourceModel.h
new file mode 100644
index 00000000..610b631c
--- /dev/null
+++ b/launcher/ui/pages/modplatform/ResourceModel.h
@@ -0,0 +1,147 @@
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
+#pragma once
+
+#include <optional>
+
+#include <QAbstractListModel>
+
+#include "QObjectPtr.h"
+
+#include "modplatform/ResourceAPI.h"
+
+#include "tasks/ConcurrentTask.h"
+
+class NetJob;
+class ResourceAPI;
+
+namespace ModPlatform {
+struct IndexedPack;
+}
+
+namespace ResourceDownload {
+
+class ResourceModel : public QAbstractListModel {
+ Q_OBJECT
+
+ Q_PROPERTY(QString search_term MEMBER m_search_term WRITE setSearchTerm)
+
+ public:
+ ResourceModel(ResourceAPI* api);
+ ~ResourceModel() override;
+
+ [[nodiscard]] auto data(const QModelIndex&, int role) const -> QVariant override;
+ [[nodiscard]] auto roleNames() const -> QHash<int, QByteArray> override;
+ bool setData(const QModelIndex& index, const QVariant& value, int role) override;
+
+ [[nodiscard]] virtual auto debugName() const -> QString;
+ [[nodiscard]] virtual auto metaEntryBase() const -> QString = 0;
+
+ [[nodiscard]] inline int rowCount(const QModelIndex& parent) const override { return parent.isValid() ? 0 : m_packs.size(); }
+ [[nodiscard]] inline int columnCount(const QModelIndex& parent) const override { return parent.isValid() ? 0 : 1; }
+ [[nodiscard]] inline auto flags(const QModelIndex& index) const -> Qt::ItemFlags override { return QAbstractListModel::flags(index); }
+
+ [[nodiscard]] bool hasActiveSearchJob() const { return m_current_search_job && m_current_search_job->isRunning(); }
+ [[nodiscard]] bool hasActiveInfoJob() const { return m_current_info_job.isRunning(); }
+ [[nodiscard]] Task::Ptr activeSearchJob() { return hasActiveSearchJob() ? m_current_search_job : nullptr; }
+
+ [[nodiscard]] auto getSortingMethods() const { return m_api->getSortingMethods(); }
+
+ public slots:
+ void fetchMore(const QModelIndex& parent) override;
+ // NOTE: Can't use [[nodiscard]] here because of https://bugreports.qt.io/browse/QTBUG-58628 on Qt 5.12
+ inline bool canFetchMore(const QModelIndex& parent) const override
+ {
+ return parent.isValid() ? false : m_search_state == SearchState::CanFetchMore;
+ }
+
+ void setSearchTerm(QString term) { m_search_term = term; }
+
+ virtual ResourceAPI::SearchArgs createSearchArguments() = 0;
+ virtual ResourceAPI::SearchCallbacks createSearchCallbacks() { return {}; }
+
+ virtual ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) = 0;
+ virtual ResourceAPI::VersionSearchCallbacks createVersionsCallbacks(QModelIndex&) { return {}; }
+
+ virtual ResourceAPI::ProjectInfoArgs createInfoArguments(QModelIndex&) = 0;
+ virtual ResourceAPI::ProjectInfoCallbacks createInfoCallbacks(QModelIndex&) { return {}; }
+
+ /** Requests the API for more entries. */
+ virtual void search();
+
+ /** Applies any processing / extra requests needed to fully load the specified entry's information. */
+ virtual void loadEntry(QModelIndex&);
+
+ /** Schedule a refresh, clearing the current state. */
+ void refresh();
+
+ /** Gets the icon at the URL for the given index. If it's not fetched yet, fetch it and update when fisinhed. */
+ std::optional<QIcon> getIcon(QModelIndex&, const QUrl&);
+
+ protected:
+ /** Resets the model's data. */
+ void clearData();
+
+ void runSearchJob(Task::Ptr);
+ void runInfoJob(Task::Ptr);
+
+ [[nodiscard]] auto getCurrentSortingMethodByIndex() const -> std::optional<ResourceAPI::SortingMethod>;
+
+ /** Converts a JSON document to a common array format.
+ *
+ * This is needed so that different providers, with different JSON structures, can be parsed
+ * uniformally. You NEED to re-implement this if you intend on using the default callbacks.
+ */
+ [[nodiscard]] virtual auto documentToArray(QJsonDocument&) const -> QJsonArray;
+
+ /** Functions to load data into a pack.
+ *
+ * Those are needed for the same reason as ddocumentToArray, and NEED to be re-implemented in the same way.
+ */
+
+ virtual void loadIndexedPack(ModPlatform::IndexedPack&, QJsonObject&);
+ virtual void loadExtraPackInfo(ModPlatform::IndexedPack&, QJsonObject&);
+ virtual void loadIndexedPackVersions(ModPlatform::IndexedPack&, QJsonArray&);
+
+ protected:
+ /* Basic search parameters */
+ enum class SearchState { None, CanFetchMore, ResetRequested, Finished } m_search_state = SearchState::None;
+ int m_next_search_offset = 0;
+ QString m_search_term;
+ unsigned int m_current_sort_index = 0;
+
+ std::unique_ptr<ResourceAPI> m_api;
+
+ // Job for searching for new entries
+ shared_qobject_ptr<Task> m_current_search_job;
+ // Job for fetching versions and extra info on existing entries
+ ConcurrentTask m_current_info_job;
+
+ shared_qobject_ptr<NetJob> m_current_icon_job;
+ QSet<QUrl> m_currently_running_icon_actions;
+ QSet<QUrl> m_failed_icon_actions;
+
+ QList<ModPlatform::IndexedPack> m_packs;
+
+ // HACK: We need this to prevent callbacks from calling the model after it has already been deleted.
+ // This leaks a tiny bit of memory per time the user has opened a resource dialog. How to make this better?
+ static QHash<ResourceModel*, bool> s_running_models;
+
+ private:
+ /* Default search request callbacks */
+ void searchRequestSucceeded(QJsonDocument&);
+ void searchRequestFailed(QString reason, int network_error_code);
+ void searchRequestAborted();
+
+ void versionRequestSucceeded(QJsonDocument&, ModPlatform::IndexedPack&, const QModelIndex&);
+
+ void infoRequestSucceeded(QJsonDocument&, ModPlatform::IndexedPack&, const QModelIndex&);
+
+ signals:
+ void versionListUpdated();
+ void projectInfoUpdated();
+};
+
+} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/ResourcePage.cpp b/launcher/ui/pages/modplatform/ResourcePage.cpp
new file mode 100644
index 00000000..bbd465bc
--- /dev/null
+++ b/launcher/ui/pages/modplatform/ResourcePage.cpp
@@ -0,0 +1,411 @@
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
+/*
+ * 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
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "ResourcePage.h"
+#include "ui_ResourcePage.h"
+
+#include <QDesktopServices>
+#include <QKeyEvent>
+
+#include "Markdown.h"
+#include "ResourceDownloadTask.h"
+
+#include "minecraft/MinecraftInstance.h"
+
+#include "ui/dialogs/ResourceDownloadDialog.h"
+#include "ui/pages/modplatform/ResourceModel.h"
+#include "ui/widgets/ProjectItem.h"
+
+namespace ResourceDownload {
+
+ResourcePage::ResourcePage(ResourceDownloadDialog* parent, BaseInstance& base_instance)
+ : QWidget(parent), m_base_instance(base_instance), m_ui(new Ui::ResourcePage), m_parent_dialog(parent), m_fetch_progress(this, false)
+{
+ m_ui->setupUi(this);
+
+ m_ui->searchEdit->installEventFilter(this);
+
+ m_ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
+ m_ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300);
+
+ m_search_timer.setTimerType(Qt::TimerType::CoarseTimer);
+ m_search_timer.setSingleShot(true);
+
+ connect(&m_search_timer, &QTimer::timeout, this, &ResourcePage::triggerSearch);
+
+ m_fetch_progress.hideIfInactive(true);
+ m_fetch_progress.setFixedHeight(24);
+ m_fetch_progress.progressFormat("");
+
+ m_ui->gridLayout_3->addWidget(&m_fetch_progress, 0, 0, 1, m_ui->gridLayout_3->columnCount());
+
+ m_ui->packView->setItemDelegate(new ProjectItemDelegate(this));
+ m_ui->packView->installEventFilter(this);
+
+ connect(m_ui->packDescription, &QTextBrowser::anchorClicked, this, &ResourcePage::openUrl);
+}
+
+ResourcePage::~ResourcePage()
+{
+ delete m_ui;
+}
+
+void ResourcePage::retranslate()
+{
+ m_ui->retranslateUi(this);
+}
+
+void ResourcePage::openedImpl()
+{
+ if (!supportsFiltering())
+ m_ui->resourceFilterButton->setVisible(false);
+
+ //: String in the search bar of the mod downloading dialog
+ m_ui->searchEdit->setPlaceholderText(tr("Search for %1...").arg(resourcesString()));
+ m_ui->resourceSelectionButton->setText(tr("Select %1 for download").arg(resourceString()));
+
+ updateSelectionButton();
+ triggerSearch();
+}
+
+auto ResourcePage::eventFilter(QObject* watched, QEvent* event) -> bool
+{
+ if (event->type() == QEvent::KeyPress) {
+ auto* keyEvent = static_cast<QKeyEvent*>(event);
+ if (watched == m_ui->searchEdit) {
+ if (keyEvent->key() == Qt::Key_Return) {
+ triggerSearch();
+ keyEvent->accept();
+ return true;
+ } else {
+ if (m_search_timer.isActive())
+ m_search_timer.stop();
+
+ m_search_timer.start(350);
+ }
+ } else if (watched == m_ui->packView) {
+ if (keyEvent->key() == Qt::Key_Return) {
+ onResourceSelected();
+
+ // To have the 'select mod' button outlined instead of the 'review and confirm' one
+ m_ui->resourceSelectionButton->setFocus(Qt::FocusReason::ShortcutFocusReason);
+ m_ui->packView->setFocus(Qt::FocusReason::NoFocusReason);
+
+ keyEvent->accept();
+ return true;
+ }
+ }
+ }
+
+ return QWidget::eventFilter(watched, event);
+}
+
+QString ResourcePage::getSearchTerm() const
+{
+ return m_ui->searchEdit->text();
+}
+
+void ResourcePage::setSearchTerm(QString term)
+{
+ m_ui->searchEdit->setText(term);
+}
+
+void ResourcePage::addSortings()
+{
+ Q_ASSERT(m_model);
+
+ auto sorts = m_model->getSortingMethods();
+ std::sort(sorts.begin(), sorts.end(), [](auto const& l, auto const& r) { return l.index < r.index; });
+
+ for (auto&& sorting : sorts)
+ m_ui->sortByBox->addItem(sorting.readable_name, QVariant(sorting.index));
+}
+
+bool ResourcePage::setCurrentPack(ModPlatform::IndexedPack pack)
+{
+ QVariant v;
+ v.setValue(pack);
+ return m_model->setData(m_ui->packView->currentIndex(), v, Qt::UserRole);
+}
+
+ModPlatform::IndexedPack ResourcePage::getCurrentPack() const
+{
+ return m_model->data(m_ui->packView->currentIndex(), Qt::UserRole).value<ModPlatform::IndexedPack>();
+}
+
+void ResourcePage::updateUi()
+{
+ auto current_pack = getCurrentPack();
+
+ QString text = "";
+ QString name = current_pack.name;
+
+ if (current_pack.websiteUrl.isEmpty())
+ text = name;
+ else
+ text = "<a href=\"" + current_pack.websiteUrl + "\">" + name + "</a>";
+
+ if (!current_pack.authors.empty()) {
+ auto authorToStr = [](ModPlatform::ModpackAuthor& author) -> QString {
+ if (author.url.isEmpty()) {
+ return author.name;
+ }
+ return QString("<a href=\"%1\">%2</a>").arg(author.url, author.name);
+ };
+ QStringList authorStrs;
+ for (auto& author : current_pack.authors) {
+ authorStrs.push_back(authorToStr(author));
+ }
+ text += "<br>" + tr(" by ") + authorStrs.join(", ");
+ }
+
+ if (current_pack.extraDataLoaded) {
+ if (!current_pack.extraData.donate.isEmpty()) {
+ text += "<br><br>" + tr("Donate information: ");
+ auto donateToStr = [](ModPlatform::DonationData& donate) -> QString {
+ return QString("<a href=\"%1\">%2</a>").arg(donate.url, donate.platform);
+ };
+ QStringList donates;
+ for (auto& donate : current_pack.extraData.donate) {
+ donates.append(donateToStr(donate));
+ }
+ text += donates.join(", ");
+ }
+
+ if (!current_pack.extraData.issuesUrl.isEmpty() || !current_pack.extraData.sourceUrl.isEmpty() ||
+ !current_pack.extraData.wikiUrl.isEmpty() || !current_pack.extraData.discordUrl.isEmpty()) {
+ text += "<br><br>" + tr("External links:") + "<br>";
+ }
+
+ if (!current_pack.extraData.issuesUrl.isEmpty())
+ text += "- " + tr("Issues: <a href=%1>%1</a>").arg(current_pack.extraData.issuesUrl) + "<br>";
+ if (!current_pack.extraData.wikiUrl.isEmpty())
+ text += "- " + tr("Wiki: <a href=%1>%1</a>").arg(current_pack.extraData.wikiUrl) + "<br>";
+ if (!current_pack.extraData.sourceUrl.isEmpty())
+ text += "- " + tr("Source code: <a href=%1>%1</a>").arg(current_pack.extraData.sourceUrl) + "<br>";
+ if (!current_pack.extraData.discordUrl.isEmpty())
+ text += "- " + tr("Discord: <a href=%1>%1</a>").arg(current_pack.extraData.discordUrl) + "<br>";
+ }
+
+ text += "<hr>";
+
+ m_ui->packDescription->setHtml(
+ text + (current_pack.extraData.body.isEmpty() ? current_pack.description : markdownToHTML(current_pack.extraData.body)));
+ m_ui->packDescription->flush();
+}
+
+void ResourcePage::updateSelectionButton()
+{
+ if (!isOpened || m_selected_version_index < 0) {
+ m_ui->resourceSelectionButton->setEnabled(false);
+ return;
+ }
+
+ m_ui->resourceSelectionButton->setEnabled(true);
+ if (!getCurrentPack().isVersionSelected(m_selected_version_index)) {
+ m_ui->resourceSelectionButton->setText(tr("Select %1 for download").arg(resourceString()));
+ } else {
+ m_ui->resourceSelectionButton->setText(tr("Deselect %1 for download").arg(resourceString()));
+ }
+}
+
+void ResourcePage::updateVersionList()
+{
+ auto current_pack = getCurrentPack();
+
+ m_ui->versionSelectionBox->blockSignals(true);
+ m_ui->versionSelectionBox->clear();
+ m_ui->versionSelectionBox->blockSignals(false);
+
+ for (int i = 0; i < current_pack.versions.size(); i++) {
+ auto& version = current_pack.versions[i];
+ if (optedOut(version))
+ continue;
+
+ m_ui->versionSelectionBox->addItem(current_pack.versions[i].version, QVariant(i));
+ }
+
+ if (m_ui->versionSelectionBox->count() == 0) {
+ m_ui->versionSelectionBox->addItem(tr("No valid version found."), QVariant(-1));
+ m_ui->resourceSelectionButton->setText(tr("Cannot select invalid version :("));
+ }
+
+ updateSelectionButton();
+}
+
+void ResourcePage::onSelectionChanged(QModelIndex curr, QModelIndex prev)
+{
+ if (!curr.isValid()) {
+ return;
+ }
+
+ auto current_pack = getCurrentPack();
+
+ bool request_load = false;
+ if (!current_pack.versionsLoaded) {
+ m_ui->resourceSelectionButton->setText(tr("Loading versions..."));
+ m_ui->resourceSelectionButton->setEnabled(false);
+
+ request_load = true;
+ } else {
+ updateVersionList();
+ }
+
+ if (!current_pack.extraDataLoaded)
+ request_load = true;
+
+ if (request_load)
+ m_model->loadEntry(curr);
+
+ updateUi();
+}
+
+void ResourcePage::onVersionSelectionChanged(QString data)
+{
+ if (data.isNull() || data.isEmpty()) {
+ m_selected_version_index = -1;
+ return;
+ }
+
+ m_selected_version_index = m_ui->versionSelectionBox->currentData().toInt();
+ updateSelectionButton();
+}
+
+void ResourcePage::addResourceToDialog(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& version)
+{
+ m_parent_dialog->addResource(pack, version);
+}
+
+void ResourcePage::removeResourceFromDialog(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& version)
+{
+ m_parent_dialog->removeResource(pack, version);
+}
+
+void ResourcePage::onResourceSelected()
+{
+ if (m_selected_version_index < 0)
+ return;
+
+ auto current_pack = getCurrentPack();
+ if (!current_pack.versionsLoaded)
+ return;
+
+ auto& version = current_pack.versions[m_selected_version_index];
+ if (version.is_currently_selected)
+ removeResourceFromDialog(current_pack, version);
+ else
+ addResourceToDialog(current_pack, version);
+
+ // Save the modified pack (and prevent warning in release build)
+ [[maybe_unused]] bool set = setCurrentPack(current_pack);
+ Q_ASSERT(set);
+
+ updateSelectionButton();
+
+ /* Force redraw on the resource list when the selection changes */
+ m_ui->packView->adjustSize();
+}
+
+void ResourcePage::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 URLs and search instead
+
+ const QString address = url.host() + url.path();
+ QRegularExpressionMatch match;
+ QString page;
+
+ auto handlers = urlHandlers();
+ for (auto it = handlers.constKeyValueBegin(); it != handlers.constKeyValueEnd(); it++) {
+ auto&& [regex, candidate] = *it;
+ if (match = QRegularExpression(regex).match(address); match.hasMatch()) {
+ page = candidate;
+ break;
+ }
+ }
+
+ if (!page.isNull()) {
+ const QString slug = match.captured(1);
+
+ // ensure the user isn't opening the same mod
+ if (slug != getCurrentPack().slug) {
+ m_parent_dialog->selectPage(page);
+
+ auto newPage = m_parent_dialog->getSelectedPage();
+
+ QLineEdit* searchEdit = newPage->m_ui->searchEdit;
+ auto model = newPage->m_model;
+ QListView* view = newPage->m_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->hasActiveSearchJob())
+ connect(model->activeSearchJob().get(), &Task::finished, jump);
+ else
+ jump();
+
+ return;
+ }
+ }
+
+ // open in the user's web browser
+ QDesktopServices::openUrl(url);
+}
+
+} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/ResourcePage.h b/launcher/ui/pages/modplatform/ResourcePage.h
new file mode 100644
index 00000000..1896d53e
--- /dev/null
+++ b/launcher/ui/pages/modplatform/ResourcePage.h
@@ -0,0 +1,111 @@
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
+#pragma once
+
+#include <QTimer>
+#include <QWidget>
+
+#include "modplatform/ModIndex.h"
+#include "modplatform/ResourceAPI.h"
+
+#include "ui/pages/BasePage.h"
+#include "ui/widgets/ProgressWidget.h"
+
+namespace Ui {
+class ResourcePage;
+}
+
+class BaseInstance;
+
+namespace ResourceDownload {
+
+class ResourceDownloadDialog;
+class ResourceModel;
+
+class ResourcePage : public QWidget, public BasePage {
+ Q_OBJECT
+ public:
+ ~ResourcePage() override;
+
+ /* Affects what the user sees */
+ [[nodiscard]] auto displayName() const -> QString override = 0;
+ [[nodiscard]] auto icon() const -> QIcon override = 0;
+ [[nodiscard]] auto id() const -> QString override = 0;
+ [[nodiscard]] auto helpPage() const -> QString override = 0;
+ [[nodiscard]] bool shouldDisplay() const override = 0;
+
+ /* Used internally */
+ [[nodiscard]] virtual auto metaEntryBase() const -> QString = 0;
+ [[nodiscard]] virtual auto debugName() const -> QString = 0;
+
+ //: The plural version of 'resource'
+ [[nodiscard]] virtual inline QString resourcesString() const { return tr("resources"); }
+ //: The singular version of 'resources'
+ [[nodiscard]] virtual inline QString resourceString() const { return tr("resource"); }
+
+ /* Features this resource's page supports */
+ [[nodiscard]] virtual bool supportsFiltering() const = 0;
+
+ void retranslate() override;
+ void openedImpl() override;
+ auto eventFilter(QObject* watched, QEvent* event) -> bool override;
+
+ /** Get the current term in the search bar. */
+ [[nodiscard]] auto getSearchTerm() const -> QString;
+ /** Programatically set the term in the search bar. */
+ void setSearchTerm(QString);
+
+ [[nodiscard]] bool setCurrentPack(ModPlatform::IndexedPack);
+ [[nodiscard]] auto getCurrentPack() const -> ModPlatform::IndexedPack;
+ [[nodiscard]] auto getDialog() const -> const ResourceDownloadDialog* { return m_parent_dialog; }
+ [[nodiscard]] auto getModel() const -> ResourceModel* { return m_model; }
+
+ protected:
+ ResourcePage(ResourceDownloadDialog* parent, BaseInstance&);
+
+ void addSortings();
+
+ public slots:
+ virtual void updateUi();
+ virtual void updateSelectionButton();
+ virtual void updateVersionList();
+
+ virtual void addResourceToDialog(ModPlatform::IndexedPack&, ModPlatform::IndexedVersion&);
+ virtual void removeResourceFromDialog(ModPlatform::IndexedPack&, ModPlatform::IndexedVersion&);
+
+ protected slots:
+ virtual void triggerSearch() {}
+
+ void onSelectionChanged(QModelIndex first, QModelIndex second);
+ void onVersionSelectionChanged(QString data);
+ void onResourceSelected();
+
+ // NOTE: Can't use [[nodiscard]] here because of https://bugreports.qt.io/browse/QTBUG-58628 on Qt 5.12
+
+ /** Associates regex expressions to pages in the order they're given in the map. */
+ virtual QMap<QString, QString> urlHandlers() const = 0;
+ virtual void openUrl(const QUrl&);
+
+ /** Whether the version is opted out or not. Currently only makes sense in CF. */
+ virtual bool optedOut(ModPlatform::IndexedVersion& ver) const { return false; };
+
+ public:
+ BaseInstance& m_base_instance;
+
+ protected:
+ Ui::ResourcePage* m_ui;
+
+ ResourceDownloadDialog* m_parent_dialog = nullptr;
+ ResourceModel* m_model = nullptr;
+
+ int m_selected_version_index = -1;
+
+ ProgressWidget m_fetch_progress;
+
+ // Used to do instant searching with a delay to cache quick changes
+ QTimer m_search_timer;
+};
+
+} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/ModPage.ui b/launcher/ui/pages/modplatform/ResourcePage.ui
index 94365aa5..73a9d3b1 100644
--- a/launcher/ui/pages/modplatform/ModPage.ui
+++ b/launcher/ui/pages/modplatform/ResourcePage.ui
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
- <class>ModPage</class>
- <widget class="QWidget" name="ModPage">
+ <class>ResourcePage</class>
+ <widget class="QWidget" name="ResourcePage">
<property name="geometry">
<rect>
<x>0</x>
@@ -49,11 +49,7 @@
</widget>
</item>
<item row="0" column="0">
- <widget class="QLineEdit" name="searchEdit">
- <property name="placeholderText">
- <string>Search for mods...</string>
- </property>
- </widget>
+ <widget class="QLineEdit" name="searchEdit"/>
</item>
<item row="2" column="0" colspan="4">
<layout class="QGridLayout" name="gridLayout_4" columnstretch="0,0,0">
@@ -74,16 +70,12 @@
<widget class="QComboBox" name="sortByBox"/>
</item>
<item row="1" column="2">
- <widget class="QPushButton" name="modSelectionButton">
- <property name="text">
- <string>Select mod for download</string>
- </property>
- </widget>
+ <widget class="QPushButton" name="resourceSelectionButton"/>
</item>
</layout>
</item>
<item row="0" column="1">
- <widget class="QPushButton" name="modFilterButton">
+ <widget class="QPushButton" name="resourceFilterButton">
<property name="text">
<string>Filter options</string>
</property>
diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp
index 2ce04068..9ad26f47 100644
--- a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp
+++ b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp
@@ -86,14 +86,14 @@ void ListModel::request()
modpacks.clear();
endResetModel();
- auto *netJob = new NetJob("Atl::Request", APPLICATION->network());
+ auto netJob = makeShared<NetJob>("Atl::Request", APPLICATION->network());
auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/json/packsnew.json");
netJob->addNetAction(Net::Download::makeByteArray(QUrl(url), &response));
jobPtr = netJob;
jobPtr->start();
- QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::requestFinished);
- QObject::connect(netJob, &NetJob::failed, this, &ListModel::requestFailed);
+ QObject::connect(netJob.get(), &NetJob::succeeded, this, &ListModel::requestFinished);
+ QObject::connect(netJob.get(), &NetJob::failed, this, &ListModel::requestFailed);
}
void ListModel::requestFinished()
diff --git a/launcher/ui/pages/modplatform/flame/FlameModModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModModel.cpp
deleted file mode 100644
index bc2c686c..00000000
--- a/launcher/ui/pages/modplatform/flame/FlameModModel.cpp
+++ /dev/null
@@ -1,31 +0,0 @@
-#include "FlameModModel.h"
-#include "Json.h"
-#include "modplatform/flame/FlameModIndex.h"
-
-namespace FlameMod {
-
-// NOLINTNEXTLINE(modernize-avoid-c-arrays)
-const char* ListModel::sorts[6]{ "Featured", "Popularity", "LastUpdated", "Name", "Author", "TotalDownloads" };
-
-void ListModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj)
-{
- FlameMod::loadIndexedPack(m, obj);
-}
-
-// We already deal with the URLs when initializing the pack, due to the API response's structure
-void ListModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj)
-{
- FlameMod::loadBody(m, obj);
-}
-
-void ListModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
-{
- FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), m_parent->m_instance);
-}
-
-auto ListModel::documentToArray(QJsonDocument& obj) const -> QJsonArray
-{
- return Json::ensureArray(obj.object(), "data");
-}
-
-} // namespace FlameMod
diff --git a/launcher/ui/pages/modplatform/flame/FlameModModel.h b/launcher/ui/pages/modplatform/flame/FlameModModel.h
deleted file mode 100644
index 6a6aef2e..00000000
--- a/launcher/ui/pages/modplatform/flame/FlameModModel.h
+++ /dev/null
@@ -1,26 +0,0 @@
-#pragma once
-
-#include "FlameModPage.h"
-
-namespace FlameMod {
-
-class ListModel : public ModPlatform::ListModel {
- Q_OBJECT
-
- public:
- ListModel(FlameModPage* parent) : ModPlatform::ListModel(parent) {}
- ~ListModel() override = default;
-
- private:
- void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
- void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
- void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override;
-
- auto documentToArray(QJsonDocument& obj) const -> QJsonArray override;
-
- // NOLINTNEXTLINE(modernize-avoid-c-arrays)
- static const char* sorts[6];
- inline auto getSorts() const -> const char** override { return sorts; };
-};
-
-} // namespace FlameMod
diff --git a/launcher/ui/pages/modplatform/flame/FlameModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModel.cpp
index 127c3de5..5961ea02 100644
--- a/launcher/ui/pages/modplatform/flame/FlameModel.cpp
+++ b/launcher/ui/pages/modplatform/flame/FlameModel.cpp
@@ -155,7 +155,7 @@ void ListModel::fetchMore(const QModelIndex& parent)
void ListModel::performPaginatedSearch()
{
- NetJob* netJob = new NetJob("Flame::Search", APPLICATION->network());
+ auto netJob = makeShared<NetJob>("Flame::Search", APPLICATION->network());
auto searchUrl = QString(
"https://api.curseforge.com/v1/mods/search?"
"gameId=432&"
@@ -172,8 +172,8 @@ void ListModel::performPaginatedSearch()
netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response));
jobPtr = netJob;
jobPtr->start();
- QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::searchRequestFinished);
- QObject::connect(netJob, &NetJob::failed, this, &ListModel::searchRequestFailed);
+ QObject::connect(netJob.get(), &NetJob::succeeded, this, &ListModel::searchRequestFinished);
+ QObject::connect(netJob.get(), &NetJob::failed, this, &ListModel::searchRequestFailed);
}
void ListModel::searchWithTerm(const QString& term, int sort)
diff --git a/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp b/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp
new file mode 100644
index 00000000..de1f2122
--- /dev/null
+++ b/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp
@@ -0,0 +1,37 @@
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
+#include "FlameResourceModels.h"
+
+#include "Json.h"
+
+#include "modplatform/flame/FlameAPI.h"
+#include "modplatform/flame/FlameModIndex.h"
+
+namespace ResourceDownload {
+
+FlameModModel::FlameModModel(BaseInstance const& base) : ModModel(base, new FlameAPI) {}
+
+void FlameModModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj)
+{
+ FlameMod::loadIndexedPack(m, obj);
+}
+
+// We already deal with the URLs when initializing the pack, due to the API response's structure
+void FlameModModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj)
+{
+ FlameMod::loadBody(m, obj);
+}
+
+void FlameModModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
+{
+ FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance);
+}
+
+auto FlameModModel::documentToArray(QJsonDocument& obj) const -> QJsonArray
+{
+ return Json::ensureArray(obj.object(), "data");
+}
+
+} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/flame/FlameResourceModels.h b/launcher/ui/pages/modplatform/flame/FlameResourceModels.h
new file mode 100644
index 00000000..625a2a7d
--- /dev/null
+++ b/launcher/ui/pages/modplatform/flame/FlameResourceModels.h
@@ -0,0 +1,30 @@
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
+#pragma once
+
+#include "ui/pages/modplatform/ModModel.h"
+#include "ui/pages/modplatform/flame/FlameResourcePages.h"
+
+namespace ResourceDownload {
+
+class FlameModModel : public ModModel {
+ Q_OBJECT
+
+ public:
+ FlameModModel(const BaseInstance&);
+ ~FlameModModel() override = default;
+
+ private:
+ [[nodiscard]] QString debugName() const override { return Flame::debugName() + " (Model)"; }
+ [[nodiscard]] QString metaEntryBase() const override { return Flame::metaEntryBase(); }
+
+ void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
+ void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
+ void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override;
+
+ auto documentToArray(QJsonDocument& obj) const -> QJsonArray override;
+};
+
+} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp
index bad78c97..485431a7 100644
--- a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp
+++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp
@@ -1,4 +1,6 @@
-// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
@@ -34,37 +36,33 @@
* limitations under the License.
*/
-#include "FlameModPage.h"
-#include "ui_ModPage.h"
+#include "FlameResourcePages.h"
+#include "ui_ResourcePage.h"
-#include "FlameModModel.h"
-#include "ui/dialogs/ModDownloadDialog.h"
+#include "FlameResourceModels.h"
+#include "ui/dialogs/ResourceDownloadDialog.h"
-FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance)
- : ModPage(dialog, instance, new FlameAPI())
+namespace ResourceDownload {
+
+FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance& instance)
+ : ModPage(dialog, instance)
{
- listModel = new FlameMod::ListModel(this);
- ui->packView->setModel(listModel);
+ m_model = new FlameModModel(instance);
+ m_ui->packView->setModel(m_model);
- // index is used to set the sorting with the flame api
- ui->sortByBox->addItem(tr("Sort by Featured"));
- ui->sortByBox->addItem(tr("Sort by Popularity"));
- ui->sortByBox->addItem(tr("Sort by Last Updated"));
- ui->sortByBox->addItem(tr("Sort by Name"));
- ui->sortByBox->addItem(tr("Sort by Author"));
- ui->sortByBox->addItem(tr("Sort by Downloads"));
+ addSortings();
// 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);
- connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FlameModPage::onVersionSelectionChanged);
- connect(ui->modSelectionButton, &QPushButton::clicked, this, &FlameModPage::onModSelected);
+ connect(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch()));
+ connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlameModPage::onSelectionChanged);
+ connect(m_ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FlameModPage::onVersionSelectionChanged);
+ connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &FlameModPage::onResourceSelected);
- ui->packDescription->setMetaEntry(metaEntryBase());
+ m_ui->packDescription->setMetaEntry(metaEntryBase());
}
-auto FlameModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders) const -> bool
+auto FlameModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, std::optional<ResourceAPI::ModLoaderTypes> loaders) const -> bool
{
Q_UNUSED(loaders);
return ver.mcVersion.contains(mineVer) && !ver.downloadUrl.isEmpty();
@@ -95,3 +93,5 @@ void FlameModPage::openUrl(const QUrl& url)
ModPage::openUrl(url);
}
+
+} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.h b/launcher/ui/pages/modplatform/flame/FlameResourcePages.h
index 58479ab9..b21a53ad 100644
--- a/launcher/ui/pages/modplatform/flame/FlameModPage.h
+++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.h
@@ -1,4 +1,6 @@
-// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
@@ -36,35 +38,48 @@
#pragma once
-#include "modplatform/ModAPI.h"
+#include "Application.h"
+
+#include "modplatform/ResourceAPI.h"
+
#include "ui/pages/modplatform/ModPage.h"
-#include "modplatform/flame/FlameAPI.h"
+namespace ResourceDownload {
+
+namespace Flame {
+static inline QString displayName() { return "CurseForge"; }
+static inline QIcon icon() { return APPLICATION->getThemedIcon("flame"); }
+static inline QString id() { return "curseforge"; }
+static inline QString debugName() { return "Flame"; }
+static inline QString metaEntryBase() { return "FlameMods"; }
+}
class FlameModPage : public ModPage {
Q_OBJECT
public:
- static FlameModPage* create(ModDownloadDialog* dialog, BaseInstance* instance)
+ static FlameModPage* create(ModDownloadDialog* dialog, BaseInstance& instance)
{
return ModPage::create<FlameModPage>(dialog, instance);
}
- FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance);
+ FlameModPage(ModDownloadDialog* dialog, BaseInstance& instance);
~FlameModPage() override = default;
- inline auto displayName() const -> QString override { return "CurseForge"; }
- inline auto icon() const -> QIcon override { return APPLICATION->getThemedIcon("flame"); }
- inline auto id() const -> QString override { return "curseforge"; }
- inline auto helpPage() const -> QString override { return "Mod-platform"; }
+ [[nodiscard]] bool shouldDisplay() const override;
- inline auto debugName() const -> QString override { return "Flame"; }
- inline auto metaEntryBase() const -> QString override { return "FlameMods"; };
+ [[nodiscard]] inline auto displayName() const -> QString override { return Flame::displayName(); }
+ [[nodiscard]] inline auto icon() const -> QIcon override { return Flame::icon(); }
+ [[nodiscard]] inline auto id() const -> QString override { return Flame::id(); }
+ [[nodiscard]] inline auto debugName() const -> QString override { return Flame::debugName(); }
+ [[nodiscard]] inline auto metaEntryBase() const -> QString override { return Flame::metaEntryBase(); }
- auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders = ModAPI::Unspecified) const -> bool override;
- bool optedOut(ModPlatform::IndexedVersion& ver) const override;
+ [[nodiscard]] inline auto helpPage() const -> QString override { return "Mod-platform"; }
- auto shouldDisplay() const -> bool override;
+ bool validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, std::optional<ResourceAPI::ModLoaderTypes> loaders = {}) const override;
+ bool optedOut(ModPlatform::IndexedVersion& ver) const override;
void openUrl(const QUrl& url) override;
};
+
+} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/ftb/FtbListModel.cpp b/launcher/ui/pages/modplatform/ftb/FtbListModel.cpp
index ce2b2b18..e8065415 100644
--- a/launcher/ui/pages/modplatform/ftb/FtbListModel.cpp
+++ b/launcher/ui/pages/modplatform/ftb/FtbListModel.cpp
@@ -109,14 +109,14 @@ void ListModel::request()
modpacks.clear();
endResetModel();
- auto *netJob = new NetJob("Ftb::Request", APPLICATION->network());
+ auto netJob = makeShared<NetJob>("Ftb::Request", APPLICATION->network());
auto url = QString(BuildConfig.MODPACKSCH_API_BASE_URL + "public/modpack/all");
netJob->addNetAction(Net::Download::makeByteArray(QUrl(url), &response));
jobPtr = netJob;
jobPtr->start();
- QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::requestFinished);
- QObject::connect(netJob, &NetJob::failed, this, &ListModel::requestFailed);
+ QObject::connect(netJob.get(), &NetJob::succeeded, this, &ListModel::requestFinished);
+ QObject::connect(netJob.get(), &NetJob::failed, this, &ListModel::requestFailed);
}
void ListModel::abortRequest()
@@ -158,14 +158,14 @@ void ListModel::requestFailed(QString reason)
void ListModel::requestPack()
{
- auto *netJob = new NetJob("Ftb::Search", APPLICATION->network());
+ auto netJob = makeShared<NetJob>("Ftb::Search", APPLICATION->network());
auto searchUrl = QString(BuildConfig.MODPACKSCH_API_BASE_URL + "public/modpack/%1").arg(currentPack);
netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response));
jobPtr = netJob;
jobPtr->start();
- QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::packRequestFinished);
- QObject::connect(netJob, &NetJob::failed, this, &ListModel::packRequestFailed);
+ QObject::connect(netJob.get(), &NetJob::succeeded, this, &ListModel::packRequestFinished);
+ QObject::connect(netJob.get(), &NetJob::failed, this, &ListModel::packRequestFailed);
}
void ListModel::packRequestFinished()
@@ -281,16 +281,16 @@ void ListModel::requestLogo(QString logo, QString url)
bool stale = entry->isStale();
- NetJob *job = new NetJob(QString("ModpacksCH Icon Download %1").arg(logo), APPLICATION->network());
+ auto job = makeShared<NetJob>(QString("ModpacksCH Icon Download %1").arg(logo), APPLICATION->network());
job->addNetAction(Net::Download::makeCached(QUrl(url), entry));
auto fullPath = entry->getFullPath();
- QObject::connect(job, &NetJob::finished, this, [this, logo, fullPath, stale]
+ QObject::connect(job.get(), &NetJob::finished, this, [this, logo, fullPath, stale]
{
logoLoaded(logo, stale);
});
- QObject::connect(job, &NetJob::failed, this, [this, logo]
+ QObject::connect(job.get(), &NetJob::failed, this, [this, logo]
{
logoFailed(logo);
});
diff --git a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp
index 6b1f6b89..2343b79f 100644
--- a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp
+++ b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp
@@ -35,6 +35,8 @@
#include "ListModel.h"
#include "Application.h"
+#include "net/HttpMetaCache.h"
+#include "net/NetJob.h"
#include "StringUtils.h"
#include <Version.h>
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp
index e6704eef..346a00b0 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp
@@ -40,7 +40,6 @@
#include "Json.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
-#include "ui/dialogs/ModDownloadDialog.h"
#include "ui/widgets/ProjectItem.h"
#include <QMessageBox>
@@ -128,7 +127,7 @@ bool ModpackListModel::setData(const QModelIndex &index, const QVariant &value,
void ModpackListModel::performPaginatedSearch()
{
// TODO: Move to standalone API
- NetJob* netJob = new NetJob("Modrinth::SearchModpack", APPLICATION->network());
+ auto netJob = makeShared<NetJob>("Modrinth::SearchModpack", APPLICATION->network());
auto searchAllUrl = QString(BuildConfig.MODRINTH_PROD_URL +
"/search?"
"offset=%1&"
@@ -143,7 +142,7 @@ void ModpackListModel::performPaginatedSearch()
netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchAllUrl), &m_all_response));
- QObject::connect(netJob, &NetJob::succeeded, this, [this] {
+ QObject::connect(netJob.get(), &NetJob::succeeded, this, [this] {
QJsonParseError parse_error_all{};
QJsonDocument doc_all = QJsonDocument::fromJson(m_all_response, &parse_error_all);
@@ -156,7 +155,7 @@ void ModpackListModel::performPaginatedSearch()
searchRequestFinished(doc_all);
});
- QObject::connect(netJob, &NetJob::failed, this, &ModpackListModel::searchRequestFailed);
+ QObject::connect(netJob.get(), &NetJob::failed, this, &ModpackListModel::searchRequestFailed);
jobPtr = netJob;
jobPtr->start();
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h
index 3be377a1..6e6be4b9 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h
@@ -38,6 +38,7 @@
#include <QAbstractListModel>
#include "modplatform/modrinth/ModrinthPackManifest.h"
+#include "net/NetJob.h"
#include "ui/pages/modplatform/modrinth/ModrinthPage.h"
class ModPage;
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp
index af92e63e..73d55133 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.cpp
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp
@@ -1,3 +1,5 @@
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
@@ -16,33 +18,33 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-#include "ModrinthModModel.h"
+#include "ModrinthResourceModels.h"
+#include "modplatform/modrinth/ModrinthAPI.h"
#include "modplatform/modrinth/ModrinthPackIndex.h"
-namespace Modrinth {
+namespace ResourceDownload {
-// NOLINTNEXTLINE(modernize-avoid-c-arrays)
-const char* ListModel::sorts[5]{ "relevance", "downloads", "follows", "updated", "newest" };
+ModrinthModModel::ModrinthModModel(BaseInstance const& base) : ModModel(base, new ModrinthAPI) {}
-void ListModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj)
+void ModrinthModModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj)
{
- Modrinth::loadIndexedPack(m, obj);
+ ::Modrinth::loadIndexedPack(m, obj);
}
-void ListModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj)
+void ModrinthModModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj)
{
- Modrinth::loadExtraPackData(m, obj);
+ ::Modrinth::loadExtraPackData(m, obj);
}
-void ListModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
+void ModrinthModModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
{
- Modrinth::loadIndexedPackVersions(m, arr, APPLICATION->network(), m_parent->m_instance);
+ ::Modrinth::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance);
}
-auto ListModel::documentToArray(QJsonDocument& obj) const -> QJsonArray
+auto ModrinthModModel::documentToArray(QJsonDocument& obj) const -> QJsonArray
{
return obj.object().value("hits").toArray();
}
-} // namespace Modrinth
+} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h
index 386897fd..56cab146 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.h
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h
@@ -1,3 +1,5 @@
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
@@ -18,27 +20,29 @@
#pragma once
-#include "ModrinthModPage.h"
+#include "ui/pages/modplatform/ModModel.h"
+#include "ui/pages/modplatform/modrinth/ModrinthResourcePages.h"
-namespace Modrinth {
+namespace ResourceDownload {
-class ListModel : public ModPlatform::ListModel {
+class ModrinthModPage;
+
+class ModrinthModModel : public ModModel {
Q_OBJECT
public:
- ListModel(ModrinthModPage* parent) : ModPlatform::ListModel(parent){};
- ~ListModel() override = default;
+ ModrinthModModel(const BaseInstance&);
+ ~ModrinthModModel() override = default;
private:
+ [[nodiscard]] QString debugName() const override { return Modrinth::debugName() + " (Model)"; }
+ [[nodiscard]] QString metaEntryBase() const override { return Modrinth::metaEntryBase(); }
+
void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override;
-
- auto documentToArray(QJsonDocument& obj) const -> QJsonArray override;
- // NOLINTNEXTLINE(modernize-avoid-c-arrays)
- static const char* sorts[5];
- inline auto getSorts() const -> const char** override { return sorts; };
+ auto documentToArray(QJsonDocument& obj) const -> QJsonArray override;
};
-} // namespace Modrinth
+} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp
index c531ea90..b82f800e 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.cpp
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp
@@ -1,3 +1,5 @@
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
@@ -33,48 +35,50 @@
* limitations under the License.
*/
-#include "ModrinthModPage.h"
+#include "ModrinthResourcePages.h"
+#include "ui_ResourcePage.h"
+
#include "modplatform/modrinth/ModrinthAPI.h"
-#include "ui_ModPage.h"
-#include "ModrinthModModel.h"
-#include "ui/dialogs/ModDownloadDialog.h"
+#include "ui/dialogs/ResourceDownloadDialog.h"
+
+#include "ui/pages/modplatform/modrinth/ModrinthResourceModels.h"
+
+namespace ResourceDownload {
-ModrinthModPage::ModrinthModPage(ModDownloadDialog* dialog, BaseInstance* instance)
- : ModPage(dialog, instance, new ModrinthAPI())
+ModrinthModPage::ModrinthModPage(ModDownloadDialog* dialog, BaseInstance& instance)
+ : ModPage(dialog, instance)
{
- listModel = new Modrinth::ListModel(this);
- ui->packView->setModel(listModel);
+ m_model = new ModrinthModModel(instance);
+ m_ui->packView->setModel(m_model);
- // index is used to set the sorting with the modrinth api
- ui->sortByBox->addItem(tr("Sort by Relevance"));
- ui->sortByBox->addItem(tr("Sort by Downloads"));
- ui->sortByBox->addItem(tr("Sort by Follows"));
- ui->sortByBox->addItem(tr("Sort by Last Updated"));
- ui->sortByBox->addItem(tr("Sort by Newest"));
+ addSortings();
// 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);
- connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &ModrinthModPage::onVersionSelectionChanged);
- connect(ui->modSelectionButton, &QPushButton::clicked, this, &ModrinthModPage::onModSelected);
+ connect(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch()));
+ connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthModPage::onSelectionChanged);
+ connect(m_ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &ModrinthModPage::onVersionSelectionChanged);
+ connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &ModrinthModPage::onResourceSelected);
- ui->packDescription->setMetaEntry(metaEntryBase());
+ m_ui->packDescription->setMetaEntry(metaEntryBase());
}
-auto ModrinthModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders) const -> bool
+auto ModrinthModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, std::optional<ResourceAPI::ModLoaderTypes> loaders) const -> bool
{
- auto loaderStrings = ModrinthAPI::getModLoaderStrings(loaders);
+ auto loaderCompatible = !loaders.has_value();
- auto loaderCompatible = false;
- for (auto remoteLoader : ver.loaders)
- {
- if (loaderStrings.contains(remoteLoader)) {
- loaderCompatible = true;
- break;
+ if (!loaderCompatible) {
+ auto loaderStrings = ModrinthAPI::getModLoaderStrings(loaders.value());
+ for (auto remoteLoader : ver.loaders)
+ {
+ if (loaderStrings.contains(remoteLoader)) {
+ loaderCompatible = true;
+ break;
+ }
}
}
+
return ver.mcVersion.contains(mineVer) && loaderCompatible;
}
@@ -82,3 +86,5 @@ auto ModrinthModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString
// other mod providers start loading before being selected, at least with
// my Qt, so we need to implement this in every derived class...
auto ModrinthModPage::shouldDisplay() const -> bool { return true; }
+
+} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h
index 40d82e6f..be38eff1 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.h
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h
@@ -1,4 +1,6 @@
-// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
/*
* PolyMC - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
@@ -35,32 +37,45 @@
#pragma once
-#include "modplatform/ModAPI.h"
+#include "Application.h"
+
+#include "modplatform/ResourceAPI.h"
+
#include "ui/pages/modplatform/ModPage.h"
-#include "modplatform/modrinth/ModrinthAPI.h"
+namespace ResourceDownload {
+
+namespace Modrinth {
+static inline QString displayName() { return "Modrinth"; }
+static inline QIcon icon() { return APPLICATION->getThemedIcon("modrinth"); }
+static inline QString id() { return "modrinth"; }
+static inline QString debugName() { return "Modrinth"; }
+static inline QString metaEntryBase() { return "ModrinthPacks"; }
+}
class ModrinthModPage : public ModPage {
Q_OBJECT
public:
- static ModrinthModPage* create(ModDownloadDialog* dialog, BaseInstance* instance)
+ static ModrinthModPage* create(ModDownloadDialog* dialog, BaseInstance& instance)
{
return ModPage::create<ModrinthModPage>(dialog, instance);
}
- ModrinthModPage(ModDownloadDialog* dialog, BaseInstance* instance);
+ ModrinthModPage(ModDownloadDialog* dialog, BaseInstance& instance);
~ModrinthModPage() override = default;
- inline auto displayName() const -> QString override { return "Modrinth"; }
- inline auto icon() const -> QIcon override { return APPLICATION->getThemedIcon("modrinth"); }
- inline auto id() const -> QString override { return "modrinth"; }
- inline auto helpPage() const -> QString override { return "Mod-platform"; }
+ [[nodiscard]] bool shouldDisplay() const override;
- inline auto debugName() const -> QString override { return "Modrinth"; }
- inline auto metaEntryBase() const -> QString override { return "ModrinthPacks"; };
+ [[nodiscard]] inline auto displayName() const -> QString override { return Modrinth::displayName(); }
+ [[nodiscard]] inline auto icon() const -> QIcon override { return Modrinth::icon(); }
+ [[nodiscard]] inline auto id() const -> QString override { return Modrinth::id(); }
+ [[nodiscard]] inline auto debugName() const -> QString override { return Modrinth::debugName(); }
+ [[nodiscard]] inline auto metaEntryBase() const -> QString override { return Modrinth::metaEntryBase(); }
- auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders = ModAPI::Unspecified) const -> bool override;
+ [[nodiscard]] inline auto helpPage() const -> QString override { return "Mod-platform"; }
- auto shouldDisplay() const -> bool override;
+ auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, std::optional<ResourceAPI::ModLoaderTypes> loaders = {}) const -> bool override;
};
+
+} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp
index b2af1ac0..50f0c72d 100644
--- a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp
+++ b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp
@@ -112,7 +112,7 @@ void Technic::ListModel::searchWithTerm(const QString& term)
void Technic::ListModel::performSearch()
{
- NetJob *netJob = new NetJob("Technic::Search", APPLICATION->network());
+ auto netJob = makeShared<NetJob>("Technic::Search", APPLICATION->network());
QString searchUrl = "";
if (currentSearchTerm.isEmpty()) {
searchUrl = QString("%1trending?build=%2")
@@ -137,8 +137,8 @@ void Technic::ListModel::performSearch()
netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response));
jobPtr = netJob;
jobPtr->start();
- QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::searchRequestFinished);
- QObject::connect(netJob, &NetJob::failed, this, &ListModel::searchRequestFailed);
+ QObject::connect(netJob.get(), &NetJob::succeeded, this, &ListModel::searchRequestFinished);
+ QObject::connect(netJob.get(), &NetJob::failed, this, &ListModel::searchRequestFailed);
}
void Technic::ListModel::searchRequestFinished()
diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp
index b15af244..859da97e 100644
--- a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp
+++ b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp
@@ -141,10 +141,10 @@ void TechnicPage::suggestCurrent()
return;
}
- NetJob *netJob = new NetJob(QString("Technic::PackMeta(%1)").arg(current.name), APPLICATION->network());
+ auto netJob = makeShared<NetJob>(QString("Technic::PackMeta(%1)").arg(current.name), APPLICATION->network());
QString slug = current.slug;
netJob->addNetAction(Net::Download::makeByteArray(QString("%1modpack/%2?build=%3").arg(BuildConfig.TECHNIC_API_BASE_URL, slug, BuildConfig.TECHNIC_API_BUILD), &response));
- QObject::connect(netJob, &NetJob::succeeded, this, [this, slug]
+ QObject::connect(netJob.get(), &NetJob::succeeded, this, [this, slug]
{
jobPtr.reset();
@@ -247,11 +247,11 @@ void TechnicPage::metadataLoaded()
// version so we can display something quicker
ui->versionSelectionBox->addItem(current.currentVersion);
- auto* netJob = new NetJob(QString("Technic::SolderMeta(%1)").arg(current.name), APPLICATION->network());
+ auto netJob = makeShared<NetJob>(QString("Technic::SolderMeta(%1)").arg(current.name), APPLICATION->network());
auto url = QString("%1/modpack/%2").arg(current.url, current.slug);
netJob->addNetAction(Net::Download::makeByteArray(QUrl(url), &response));
- QObject::connect(netJob, &NetJob::succeeded, this, &TechnicPage::onSolderLoaded);
+ QObject::connect(netJob.get(), &NetJob::succeeded, this, &TechnicPage::onSolderLoaded);
jobPtr = netJob;
jobPtr->start();
diff --git a/launcher/ui/widgets/ProgressWidget.cpp b/launcher/ui/widgets/ProgressWidget.cpp
index b60d9a7a..f736af08 100644
--- a/launcher/ui/widgets/ProgressWidget.cpp
+++ b/launcher/ui/widgets/ProgressWidget.cpp
@@ -39,7 +39,7 @@ void ProgressWidget::progressFormat(QString format)
m_bar->setFormat(format);
}
-void ProgressWidget::watch(Task* task)
+void ProgressWidget::watch(const Task* task)
{
if (!task)
return;
@@ -54,14 +54,17 @@ void ProgressWidget::watch(Task* task)
connect(m_task, &Task::progress, this, &ProgressWidget::handleTaskProgress);
connect(m_task, &Task::destroyed, this, &ProgressWidget::taskDestroyed);
- show();
+ if (m_task->isRunning())
+ show();
+ else
+ connect(m_task, &Task::started, this, &ProgressWidget::show);
}
-void ProgressWidget::start(Task* task)
+void ProgressWidget::start(const Task* task)
{
watch(task);
if (!m_task->isRunning())
- QMetaObject::invokeMethod(m_task, "start", Qt::QueuedConnection);
+ QMetaObject::invokeMethod(const_cast<Task*>(m_task), "start", Qt::QueuedConnection);
}
bool ProgressWidget::exec(std::shared_ptr<Task> task)
diff --git a/launcher/ui/widgets/ProgressWidget.h b/launcher/ui/widgets/ProgressWidget.h
index 4d9097b8..b0458f33 100644
--- a/launcher/ui/widgets/ProgressWidget.h
+++ b/launcher/ui/widgets/ProgressWidget.h
@@ -27,10 +27,10 @@ class ProgressWidget : public QWidget {
public slots:
/** Watch the progress of a task. */
- void watch(Task* task);
+ void watch(const Task* task);
/** Watch the progress of a task, and start it if needed */
- void start(Task* task);
+ void start(const Task* task);
/** Blocking way of waiting for a task to finish. */
bool exec(std::shared_ptr<Task> task);
@@ -50,7 +50,7 @@ class ProgressWidget : public QWidget {
private:
QLabel* m_label = nullptr;
QProgressBar* m_bar = nullptr;
- Task* m_task = nullptr;
+ const Task* m_task = nullptr;
bool m_hide_if_inactive = false;
};
diff --git a/launcher/ui/widgets/WideBar.cpp b/launcher/ui/widgets/WideBar.cpp
index 428be563..ac34e3aa 100644
--- a/launcher/ui/widgets/WideBar.cpp
+++ b/launcher/ui/widgets/WideBar.cpp
@@ -7,12 +7,20 @@
class ActionButton : public QToolButton {
Q_OBJECT
public:
- ActionButton(QAction* action, QWidget* parent = nullptr) : QToolButton(parent), m_action(action)
+ ActionButton(QAction* action, QWidget* parent = nullptr, bool use_default_action = false) : QToolButton(parent),
+ m_action(action), m_use_default_action(use_default_action)
{
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
-
+ setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
+ // workaround for breeze and breeze forks
+ setProperty("_kde_toolButton_alignment", Qt::AlignLeft);
+
+ if (m_use_default_action) {
+ setDefaultAction(action);
+ } else {
+ connect(this, &ActionButton::clicked, action, &QAction::trigger);
+ }
connect(action, &QAction::changed, this, &ActionButton::actionChanged);
- connect(this, &ActionButton::clicked, action, &QAction::trigger);
actionChanged();
};
@@ -20,17 +28,24 @@ class ActionButton : public QToolButton {
void actionChanged()
{
setEnabled(m_action->isEnabled());
- setChecked(m_action->isChecked());
- setCheckable(m_action->isCheckable());
- setText(m_action->text());
- setIcon(m_action->icon());
- setToolTip(m_action->toolTip());
- setHidden(!m_action->isVisible());
+ // better pop up mode
+ if (m_action->menu()) {
+ setPopupMode(QToolButton::MenuButtonPopup);
+ }
+ if (!m_use_default_action) {
+ setChecked(m_action->isChecked());
+ setCheckable(m_action->isCheckable());
+ setText(m_action->text());
+ setIcon(m_action->icon());
+ setToolTip(m_action->toolTip());
+ setHidden(!m_action->isVisible());
+ }
setFocusPolicy(Qt::NoFocus);
}
private:
QAction* m_action;
+ bool m_use_default_action;
};
WideBar::WideBar(const QString& title, QWidget* parent) : QToolBar(title, parent)
@@ -54,7 +69,7 @@ WideBar::WideBar(QWidget* parent) : QToolBar(parent)
void WideBar::addAction(QAction* action)
{
BarEntry entry;
- entry.bar_action = addWidget(new ActionButton(action, this));
+ entry.bar_action = addWidget(new ActionButton(action, this, m_use_default_action));
entry.menu_action = action;
entry.type = BarEntry::Type::Action;
@@ -86,7 +101,7 @@ void WideBar::insertActionBefore(QAction* before, QAction* action)
return;
BarEntry entry;
- entry.bar_action = insertWidget(iter->bar_action, new ActionButton(action, this));
+ entry.bar_action = insertWidget(iter->bar_action, new ActionButton(action, this, m_use_default_action));
entry.menu_action = action;
entry.type = BarEntry::Type::Action;
@@ -102,7 +117,7 @@ void WideBar::insertActionAfter(QAction* after, QAction* action)
return;
BarEntry entry;
- entry.bar_action = insertWidget((iter + 1)->bar_action, new ActionButton(action, this));
+ entry.bar_action = insertWidget((iter + 1)->bar_action, new ActionButton(action, this, m_use_default_action));
entry.menu_action = action;
entry.type = BarEntry::Type::Action;
@@ -111,6 +126,15 @@ void WideBar::insertActionAfter(QAction* after, QAction* action)
m_menu_state = MenuState::Dirty;
}
+void WideBar::insertWidgetBefore(QAction* before, QWidget* widget)
+{
+ auto iter = getMatching(before);
+ if (iter == m_entries.end())
+ return;
+
+ insertWidget(iter->bar_action, widget);
+}
+
void WideBar::insertSpacer(QAction* action)
{
auto iter = getMatching(action);
@@ -133,7 +157,7 @@ void WideBar::insertSeparator(QAction* before)
return;
BarEntry entry;
- entry.bar_action = QToolBar::insertSeparator(before);
+ entry.bar_action = QToolBar::insertSeparator(iter->bar_action);
entry.type = BarEntry::Type::Separator;
m_entries.insert(iter, entry);
@@ -180,6 +204,10 @@ void WideBar::showVisibilityMenu(QPoint const& position)
m_bar_menu->clear();
+ m_bar_menu->addActions(m_context_menu_actions);
+
+ m_bar_menu->addSeparator()->setText(tr("Customize toolbar actions"));
+
for (auto& entry : m_entries) {
if (entry.type != BarEntry::Type::Action)
continue;
@@ -206,6 +234,10 @@ void WideBar::showVisibilityMenu(QPoint const& position)
m_bar_menu->popup(mapToGlobal(position));
}
+void WideBar::addContextMenuAction(QAction* action) {
+ m_context_menu_actions.append(action);
+}
+
[[nodiscard]] QByteArray WideBar::getVisibilityState() const
{
QByteArray state;
diff --git a/launcher/ui/widgets/WideBar.h b/launcher/ui/widgets/WideBar.h
index a0a7896c..c47f3a59 100644
--- a/launcher/ui/widgets/WideBar.h
+++ b/launcher/ui/widgets/WideBar.h
@@ -9,6 +9,9 @@
class WideBar : public QToolBar {
Q_OBJECT
+ // Why: so we can enable / disable alt shortcuts in toolbuttons
+ // with toolbuttons using setDefaultAction, theres no alt shortcuts
+ Q_PROPERTY(bool useDefaultAction MEMBER m_use_default_action)
public:
explicit WideBar(const QString& title, QWidget* parent = nullptr);
@@ -22,10 +25,13 @@ class WideBar : public QToolBar {
void insertSeparator(QAction* before);
void insertActionBefore(QAction* before, QAction* action);
void insertActionAfter(QAction* after, QAction* action);
+ void insertWidgetBefore(QAction* before, QWidget* widget);
QMenu* createContextMenu(QWidget* parent = nullptr, const QString& title = QString());
void showVisibilityMenu(const QPoint&);
+ void addContextMenuAction(QAction* action);
+
// Ideally we would use a QBitArray for this, but it doesn't support string conversion,
// so using it in settings is very messy.
@@ -48,6 +54,10 @@ class WideBar : public QToolBar {
private:
QList<BarEntry> m_entries;
+ QList<QAction*> m_context_menu_actions;
+
+ bool m_use_default_action = false;
+
// Menu to toggle visibility from buttons in the bar
std::unique_ptr<QMenu> m_bar_menu = nullptr;
enum class MenuState { Fresh, Dirty } m_menu_state = MenuState::Dirty;
diff --git a/launcher/updater/DownloadTask.cpp b/launcher/updater/DownloadTask.cpp
deleted file mode 100644
index 48fe767a..00000000
--- a/launcher/updater/DownloadTask.cpp
+++ /dev/null
@@ -1,177 +0,0 @@
-/* Copyright 2013-2021 MultiMC Contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include "DownloadTask.h"
-
-#include "updater/UpdateChecker.h"
-#include "GoUpdate.h"
-#include "net/NetJob.h"
-
-#include <QFile>
-#include <QTemporaryDir>
-#include <QCryptographicHash>
-
-namespace GoUpdate
-{
-
-DownloadTask::DownloadTask(
- shared_qobject_ptr<QNetworkAccessManager> network,
- Status status,
- QString target,
- QObject *parent
-) : Task(parent), m_updateFilesDir(target), m_network(network)
-{
- m_status = status;
-
- m_updateFilesDir.setAutoRemove(false);
-}
-
-void DownloadTask::executeTask()
-{
- loadVersionInfo();
-}
-
-void DownloadTask::loadVersionInfo()
-{
- setStatus(tr("Loading version information..."));
-
- NetJob *netJob = new NetJob("Version Info", m_network);
-
- // Find the index URL.
- QUrl newIndexUrl = QUrl(m_status.newRepoUrl).resolved(QString::number(m_status.newVersionId) + ".json");
- qDebug() << m_status.newRepoUrl << " turns into " << newIndexUrl;
-
- netJob->addNetAction(m_newVersionFileListDownload = Net::Download::makeByteArray(newIndexUrl, &newVersionFileListData));
-
- // If we have a current version URL, get that one too.
- if (!m_status.currentRepoUrl.isEmpty())
- {
- QUrl cIndexUrl = QUrl(m_status.currentRepoUrl).resolved(QString::number(m_status.currentVersionId) + ".json");
- netJob->addNetAction(m_currentVersionFileListDownload = Net::Download::makeByteArray(cIndexUrl, &currentVersionFileListData));
- qDebug() << m_status.currentRepoUrl << " turns into " << cIndexUrl;
- }
-
- // connect signals and start the job
- connect(netJob, &NetJob::succeeded, this, &DownloadTask::processDownloadedVersionInfo);
- connect(netJob, &NetJob::failed, this, &DownloadTask::vinfoDownloadFailed);
- m_vinfoNetJob.reset(netJob);
- netJob->start();
-}
-
-void DownloadTask::vinfoDownloadFailed()
-{
- // Something failed. We really need the second download (current version info), so parse
- // downloads anyways as long as the first one succeeded.
- if (m_newVersionFileListDownload->wasSuccessful())
- {
- processDownloadedVersionInfo();
- return;
- }
-
- // TODO: Give a more detailed error message.
- qCritical() << "Failed to download version info files.";
- emitFailed(tr("Failed to download version info files."));
-}
-
-void DownloadTask::processDownloadedVersionInfo()
-{
- VersionFileList m_currentVersionFileList;
- VersionFileList m_newVersionFileList;
-
- setStatus(tr("Reading file list for new version..."));
- qDebug() << "Reading file list for new version...";
- QString error;
- if (!parseVersionInfo(newVersionFileListData, m_newVersionFileList, error))
- {
- qCritical() << error;
- emitFailed(error);
- return;
- }
-
- // if we have the current version info, use it.
- if (m_currentVersionFileListDownload && m_currentVersionFileListDownload->wasSuccessful())
- {
- setStatus(tr("Reading file list for current version..."));
- qDebug() << "Reading file list for current version...";
- // if this fails, it's not a complete loss.
- QString error;
- if(!parseVersionInfo( currentVersionFileListData, m_currentVersionFileList, error))
- {
- qDebug() << error << "This is not a fatal error.";
- }
- }
-
- // We don't need this any more.
- m_currentVersionFileListDownload.reset();
- m_newVersionFileListDownload.reset();
- m_vinfoNetJob.reset();
-
- setStatus(tr("Processing file lists - figuring out how to install the update..."));
-
- // make a new netjob for the actual update files
- NetJob::Ptr netJob = new NetJob("Update Files", m_network);
-
- // fill netJob and operationList
- if (!processFileLists(m_currentVersionFileList, m_newVersionFileList, m_status.rootPath, m_updateFilesDir.path(), netJob, m_operations))
- {
- emitFailed(tr("Failed to process update lists..."));
- return;
- }
-
- // Now start the download.
- QObject::connect(netJob.get(), &NetJob::succeeded, this, &DownloadTask::fileDownloadFinished);
- QObject::connect(netJob.get(), &NetJob::progress, this, &DownloadTask::fileDownloadProgressChanged);
- QObject::connect(netJob.get(), &NetJob::failed, this, &DownloadTask::fileDownloadFailed);
-
- if(netJob->size() == 1) // Translation issues... see https://github.com/MultiMC/Launcher/issues/1701
- {
- setStatus(tr("Downloading one update file."));
- }
- else
- {
- setStatus(tr("Downloading %1 update files.").arg(QString::number(netJob->size())));
- }
- qDebug() << "Begin downloading update files to" << m_updateFilesDir.path();
- m_filesNetJob = netJob;
- m_filesNetJob->start();
-}
-
-void DownloadTask::fileDownloadFinished()
-{
- emitSucceeded();
-}
-
-void DownloadTask::fileDownloadFailed(QString reason)
-{
- qCritical() << "Failed to download update files:" << reason;
- emitFailed(tr("Failed to download update files: %1").arg(reason));
-}
-
-void DownloadTask::fileDownloadProgressChanged(qint64 current, qint64 total)
-{
- setProgress(current, total);
-}
-
-QString DownloadTask::updateFilesDir()
-{
- return m_updateFilesDir.path();
-}
-
-OperationList DownloadTask::operations()
-{
- return m_operations;
-}
-
-}
diff --git a/launcher/updater/DownloadTask.h b/launcher/updater/DownloadTask.h
deleted file mode 100644
index 19a6265c..00000000
--- a/launcher/updater/DownloadTask.h
+++ /dev/null
@@ -1,100 +0,0 @@
-/* Copyright 2013-2021 MultiMC Contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#pragma once
-
-#include "tasks/Task.h"
-#include "net/NetJob.h"
-#include "GoUpdate.h"
-
-namespace GoUpdate
-{
-/*!
- * The DownloadTask is a task that takes a given version ID and repository URL,
- * downloads that version's files from the repository, and prepares to install them.
- */
-class DownloadTask : public Task
-{
- Q_OBJECT
-
-public:
- /**
- * Create a download task
- *
- * target is a template - XXXXXX at the end will be replaced with a random generated string, ensuring uniqueness
- */
- explicit DownloadTask(shared_qobject_ptr<QNetworkAccessManager> network, Status status, QString target, QObject* parent = 0);
- virtual ~DownloadTask() {};
-
- /// Get the directory that will contain the update files.
- QString updateFilesDir();
-
- /// Get the list of operations that should be done
- OperationList operations();
-
- /// set updater download behavior
- void setUseLocalUpdater(bool useLocal);
-
-protected:
- //! Entry point for tasks.
- virtual void executeTask() override;
-
- /*!
- * Downloads the version info files from the repository.
- * The files for both the current build, and the build that we're updating to need to be downloaded.
- * If the current version's info file can't be found, Prism Launcher will not delete files that
- * were removed between versions. It will still replace files that have changed, however.
- * Note that although the repository URL for the current version is not given to the update task,
- * the task will attempt to look it up in the UpdateChecker's channel list.
- * If an error occurs here, the function will call emitFailed and return false.
- */
- void loadVersionInfo();
-
- NetJob::Ptr m_vinfoNetJob;
- QByteArray currentVersionFileListData;
- QByteArray newVersionFileListData;
- Net::Download::Ptr m_currentVersionFileListDownload;
- Net::Download::Ptr m_newVersionFileListDownload;
-
- NetJob::Ptr m_filesNetJob;
-
- Status m_status;
-
- OperationList m_operations;
-
- /*!
- * Temporary directory to store update files in.
- * This will be set to not auto delete. Task will fail if this fails to be created.
- */
- QTemporaryDir m_updateFilesDir;
-
-protected slots:
- /*!
- * This function is called when version information is finished downloading
- * and at least the new file list download succeeded
- */
- void processDownloadedVersionInfo();
- void vinfoDownloadFailed();
-
- void fileDownloadFinished();
- void fileDownloadFailed(QString reason);
- void fileDownloadProgressChanged(qint64 current, qint64 total);
-
-private:
- shared_qobject_ptr<QNetworkAccessManager> m_network;
-};
-
-}
-
diff --git a/launcher/updater/GoUpdate.cpp b/launcher/updater/GoUpdate.cpp
deleted file mode 100644
index 4bc7dfa9..00000000
--- a/launcher/updater/GoUpdate.cpp
+++ /dev/null
@@ -1,198 +0,0 @@
-#include "GoUpdate.h"
-#include <QDebug>
-#include <QDomDocument>
-#include <QFile>
-#include <FileSystem.h>
-
-#include "net/Download.h"
-#include "net/ChecksumValidator.h"
-
-namespace GoUpdate
-{
-
-bool parseVersionInfo(const QByteArray &data, VersionFileList &list, QString &error)
-{
- QJsonParseError jsonError;
- QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError);
- if (jsonError.error != QJsonParseError::NoError)
- {
- error = QString("Failed to parse version info JSON: %1 at %2")
- .arg(jsonError.errorString())
- .arg(jsonError.offset);
- qCritical() << error;
- return false;
- }
-
- QJsonObject json = jsonDoc.object();
-
- qDebug() << data;
- qDebug() << "Loading version info from JSON.";
- QJsonArray filesArray = json.value("Files").toArray();
- for (QJsonValue fileValue : filesArray)
- {
- QJsonObject fileObj = fileValue.toObject();
-
- QString file_path = fileObj.value("Path").toString();
-
- VersionFileEntry file{file_path, fileObj.value("Perms").toVariant().toInt(),
- FileSourceList(), fileObj.value("MD5").toString(), };
- qDebug() << "File" << file.path << "with perms" << file.mode;
-
- QJsonArray sourceArray = fileObj.value("Sources").toArray();
- for (QJsonValue val : sourceArray)
- {
- QJsonObject sourceObj = val.toObject();
-
- QString type = sourceObj.value("SourceType").toString();
- if (type == "http")
- {
- file.sources.append(FileSource("http", sourceObj.value("Url").toString()));
- }
- else
- {
- qWarning() << "Unknown source type" << type << "ignored.";
- }
- }
-
- qDebug() << "Loaded info for" << file.path;
-
- list.append(file);
- }
-
- return true;
-}
-
-bool processFileLists
-(
- const VersionFileList &currentVersion,
- const VersionFileList &newVersion,
- const QString &rootPath,
- const QString &tempPath,
- NetJob::Ptr job,
- OperationList &ops
-)
-{
- // First, if we've loaded the current version's file list, we need to iterate through it and
- // delete anything in the current one version's list that isn't in the new version's list.
- for (VersionFileEntry entry : currentVersion)
- {
- QFileInfo toDelete(FS::PathCombine(rootPath, entry.path));
- if (!toDelete.exists())
- {
- qCritical() << "Expected file " << toDelete.absoluteFilePath()
- << " doesn't exist!";
- }
- bool keep = false;
-
- //
- for (VersionFileEntry newEntry : newVersion)
- {
- if (newEntry.path == entry.path)
- {
- qDebug() << "Not deleting" << entry.path
- << "because it is still present in the new version.";
- keep = true;
- break;
- }
- }
-
- // If the loop reaches the end and we didn't find a match, delete the file.
- if (!keep)
- {
- if (toDelete.exists())
- ops.append(Operation::DeleteOp(entry.path));
- }
- }
-
- // Next, check each file in Prism Launcher's folder and see if we need to update them.
- for (VersionFileEntry entry : newVersion)
- {
- // TODO: Let's not MD5sum a ton of files on the GUI thread. We should probably find a
- // way to do this in the background.
- QString fileMD5;
- QString realEntryPath = FS::PathCombine(rootPath, entry.path);
- QFile entryFile(realEntryPath);
- QFileInfo entryInfo(realEntryPath);
-
- bool needs_upgrade = false;
- if (!entryFile.exists())
- {
- needs_upgrade = true;
- }
- else
- {
- bool pass = true;
- if (!entryInfo.isReadable())
- {
- qCritical() << "File " << realEntryPath << " is not readable.";
- pass = false;
- }
- if (!entryInfo.isWritable())
- {
- qCritical() << "File " << realEntryPath << " is not writable.";
- pass = false;
- }
- if (!entryFile.open(QFile::ReadOnly))
- {
- qCritical() << "File " << realEntryPath << " cannot be opened for reading.";
- pass = false;
- }
- if (!pass)
- {
- ops.clear();
- return false;
- }
- }
-
- if(!needs_upgrade)
- {
- QCryptographicHash hash(QCryptographicHash::Md5);
- auto foo = entryFile.readAll();
-
- hash.addData(foo);
- fileMD5 = hash.result().toHex();
- if ((fileMD5 != entry.md5))
- {
- qDebug() << "MD5Sum does not match!";
- qDebug() << "Expected:'" << entry.md5 << "'";
- qDebug() << "Got: '" << fileMD5 << "'";
- needs_upgrade = true;
- }
- }
-
- // skip file. it doesn't need an upgrade.
- if (!needs_upgrade)
- {
- qDebug() << "File" << realEntryPath << " does not need updating.";
- continue;
- }
-
- // yep. this file actually needs an upgrade. PROCEED.
- qDebug() << "Found file" << realEntryPath << " that needs updating.";
-
- // Go through the sources list and find one to use.
- // TODO: Make a NetAction that takes a source list and tries each of them until one
- // works. For now, we'll just use the first http one.
- for (FileSource source : entry.sources)
- {
- if (source.type != "http")
- continue;
-
- qDebug() << "Will download" << entry.path << "from" << source.url;
-
- // Download it to updatedir/<filepath>-<md5> where filepath is the file's
- // path with slashes replaced by underscores.
- QString dlPath = FS::PathCombine(tempPath, QString(entry.path).replace("/", "_"));
-
- // We need to download the file to the updatefiles folder and add a task
- // to copy it to its install path.
- auto download = Net::Download::makeFile(source.url, dlPath);
- auto rawMd5 = QByteArray::fromHex(entry.md5.toLatin1());
- download->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, rawMd5));
- job->addNetAction(download);
- ops.append(Operation::CopyOp(dlPath, entry.path, entry.mode));
- }
- }
- return true;
-}
-}
diff --git a/launcher/updater/GoUpdate.h b/launcher/updater/GoUpdate.h
deleted file mode 100644
index 46a679ef..00000000
--- a/launcher/updater/GoUpdate.h
+++ /dev/null
@@ -1,125 +0,0 @@
-#pragma once
-#include <QByteArray>
-#include <net/NetJob.h>
-
-namespace GoUpdate
-{
-
-/**
- * A temporary object exchanged between updated checker and the actual update task
- */
-struct Status
-{
- bool updateAvailable = false;
-
- int newVersionId = -1;
- QString newRepoUrl;
-
- int currentVersionId = -1;
- QString currentRepoUrl;
-
- // path to the root of the application
- QString rootPath;
-};
-
-/**
- * Struct that describes an entry in a VersionFileEntry's `Sources` list.
- */
-struct FileSource
-{
- FileSource(QString type, QString url, QString compression="")
- {
- this->type = type;
- this->url = url;
- this->compressionType = compression;
- }
-
- bool operator==(const FileSource &f2) const
- {
- return type == f2.type && url == f2.url && compressionType == f2.compressionType;
- }
-
- QString type;
- QString url;
- QString compressionType;
-};
-typedef QList<FileSource> FileSourceList;
-
-/**
- * Structure that describes an entry in a GoUpdate version's `Files` list.
- */
-struct VersionFileEntry
-{
- QString path;
- int mode;
- FileSourceList sources;
- QString md5;
- bool operator==(const VersionFileEntry &v2) const
- {
- return path == v2.path && mode == v2.mode && sources == v2.sources && md5 == v2.md5;
- }
-};
-typedef QList<VersionFileEntry> VersionFileList;
-
-/**
- * Structure that describes an operation to perform when installing updates.
- */
-struct Operation
-{
- static Operation CopyOp(QString from, QString to, int fmode=0644)
- {
- return Operation{OP_REPLACE, from, to, fmode};
- }
- static Operation DeleteOp(QString file)
- {
- return Operation{OP_DELETE, QString(), file, 0644};
- }
-
- // FIXME: for some types, some of the other fields are irrelevant!
- bool operator==(const Operation &u2) const
- {
- return type == u2.type &&
- source == u2.source &&
- destination == u2.destination &&
- destinationMode == u2.destinationMode;
- }
-
- //! Specifies the type of operation that this is.
- enum Type
- {
- OP_REPLACE,
- OP_DELETE,
- } type;
-
- //! The source file, if any
- QString source;
-
- //! The destination file.
- QString destination;
-
- //! The mode to change the destination file to.
- int destinationMode;
-};
-typedef QList<Operation> OperationList;
-
-/**
- * Loads the file list from the given version info JSON object into the given list.
- */
-bool parseVersionInfo(const QByteArray &data, VersionFileList& list, QString &error);
-
-/*!
- * Takes a list of file entries for the current version's files and the new version's files
- * and populates the downloadList and operationList with information about how to download and install the update.
- */
-bool processFileLists
-(
- const VersionFileList &currentVersion,
- const VersionFileList &newVersion,
- const QString &rootPath,
- const QString &tempPath,
- NetJob::Ptr job,
- OperationList &ops
-);
-
-}
-Q_DECLARE_METATYPE(GoUpdate::Status)
diff --git a/launcher/updater/MacSparkleUpdater.h b/launcher/updater/MacSparkleUpdater.h
index d50dbd68..cee19f7c 100644
--- a/launcher/updater/MacSparkleUpdater.h
+++ b/launcher/updater/MacSparkleUpdater.h
@@ -119,8 +119,6 @@ private:
class Private;
Private *priv;
-
- void loadChannelsFromSettings();
};
#endif //LAUNCHER_MACSPARKLEUPDATER_H
diff --git a/launcher/updater/MacSparkleUpdater.mm b/launcher/updater/MacSparkleUpdater.mm
index ca6da55a..07337176 100644
--- a/launcher/updater/MacSparkleUpdater.mm
+++ b/launcher/updater/MacSparkleUpdater.mm
@@ -106,8 +106,6 @@ MacSparkleUpdater::MacSparkleUpdater()
priv->updaterObserver.callback = ^(bool canCheck) {
emit canCheckForUpdatesChanged(canCheck);
};
-
- loadChannelsFromSettings();
}
MacSparkleUpdater::~MacSparkleUpdater()
@@ -165,7 +163,6 @@ void MacSparkleUpdater::setUpdateCheckInterval(double seconds)
void MacSparkleUpdater::clearAllowedChannels()
{
priv->updaterDelegate.allowedChannels = [NSSet set];
- APPLICATION->settings()->set("UpdateChannel", "");
}
void MacSparkleUpdater::setAllowedChannel(const QString &channel)
@@ -178,7 +175,6 @@ void MacSparkleUpdater::setAllowedChannel(const QString &channel)
NSSet<NSString *> *nsChannels = [NSSet setWithObject:channel.toNSString()];
priv->updaterDelegate.allowedChannels = nsChannels;
- APPLICATION->settings()->set("UpdateChannel", channel);
}
void MacSparkleUpdater::setAllowedChannels(const QSet<QString> &channels)
@@ -199,7 +195,6 @@ void MacSparkleUpdater::setAllowedChannels(const QSet<QString> &channels)
}
priv->updaterDelegate.allowedChannels = nsChannels;
- APPLICATION->settings()->set("UpdateChannel", channelsConfig.trimmed());
}
void MacSparkleUpdater::setBetaAllowed(bool allowed)
@@ -213,10 +208,3 @@ void MacSparkleUpdater::setBetaAllowed(bool allowed)
clearAllowedChannels();
}
}
-
-void MacSparkleUpdater::loadChannelsFromSettings()
-{
- QStringList channelList = APPLICATION->settings()->get("UpdateChannel").toString().split(" ");
- QSet<QString> channels(channelList.begin(), channelList.end());
- setAllowedChannels(channels);
-}
diff --git a/launcher/updater/UpdateChecker.cpp b/launcher/updater/UpdateChecker.cpp
deleted file mode 100644
index 78d979ff..00000000
--- a/launcher/updater/UpdateChecker.cpp
+++ /dev/null
@@ -1,296 +0,0 @@
-/* Copyright 2013-2021 MultiMC Contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include "UpdateChecker.h"
-
-#include <QJsonObject>
-#include <QJsonArray>
-#include <QJsonValue>
-#include <QDebug>
-
-#define API_VERSION 0
-#define CHANLIST_FORMAT 0
-
-#include "BuildConfig.h"
-
-UpdateChecker::UpdateChecker(shared_qobject_ptr<QNetworkAccessManager> nam, QString channelUrl, QString currentChannel)
-{
- m_network = nam;
- m_channelUrl = channelUrl;
- m_currentChannel = currentChannel;
-
-#ifdef Q_OS_MAC
- m_externalUpdater = new MacSparkleUpdater();
-#endif
-}
-
-QList<UpdateChecker::ChannelListEntry> UpdateChecker::getChannelList() const
-{
- return m_channels;
-}
-
-bool UpdateChecker::hasChannels() const
-{
- return !m_channels.isEmpty();
-}
-
-ExternalUpdater* UpdateChecker::getExternalUpdater()
-{
- return m_externalUpdater;
-}
-
-void UpdateChecker::checkForUpdate(const QString& updateChannel, bool notifyNoUpdate)
-{
- if (m_externalUpdater)
- {
- m_externalUpdater->setBetaAllowed(updateChannel == "beta");
- if (notifyNoUpdate)
- {
- qDebug() << "Checking for updates.";
- m_externalUpdater->checkForUpdates();
- } else
- {
- // The updater library already handles automatic update checks.
- return;
- }
- }
- else
- {
- qDebug() << "Checking for updates.";
- // If the channel list hasn't loaded yet, load it and defer checking for updates until
- // later.
- if (!m_chanListLoaded)
- {
- qDebug() << "Channel list isn't loaded yet. Loading channel list and deferring update check.";
- m_checkUpdateWaiting = true;
- m_deferredUpdateChannel = updateChannel;
- updateChanList(notifyNoUpdate);
- return;
- }
-
- if (m_updateChecking)
- {
- qDebug() << "Ignoring update check request. Already checking for updates.";
- return;
- }
-
- // Find the desired channel within the channel list and get its repo URL. If if cannot be
- // found, error.
- QString stableUrl;
- m_newRepoUrl = "";
- for (ChannelListEntry entry: m_channels)
- {
- qDebug() << "channelEntry = " << entry.id;
- if (entry.id == "stable")
- {
- stableUrl = entry.url;
- }
- if (entry.id == updateChannel)
- {
- m_newRepoUrl = entry.url;
- qDebug() << "is intended update channel: " << entry.id;
- }
- if (entry.id == m_currentChannel)
- {
- m_currentRepoUrl = entry.url;
- qDebug() << "is current update channel: " << entry.id;
- }
- }
-
- qDebug() << "m_repoUrl = " << m_newRepoUrl;
-
- if (m_newRepoUrl.isEmpty())
- {
- qWarning() << "m_repoUrl was empty. defaulting to 'stable': " << stableUrl;
- m_newRepoUrl = stableUrl;
- }
-
- // If nothing applies, error
- if (m_newRepoUrl.isEmpty())
- {
- qCritical() << "failed to select any update repository for: " << updateChannel;
- emit updateCheckFailed();
- return;
- }
-
- m_updateChecking = true;
-
- QUrl indexUrl = QUrl(m_newRepoUrl).resolved(QUrl("index.json"));
-
- indexJob = new NetJob("GoUpdate Repository Index", m_network);
- indexJob->addNetAction(Net::Download::makeByteArray(indexUrl, &indexData));
- connect(indexJob.get(), &NetJob::succeeded, [this, notifyNoUpdate]() { updateCheckFinished(notifyNoUpdate); });
- connect(indexJob.get(), &NetJob::failed, this, &UpdateChecker::updateCheckFailed);
- indexJob->start();
- }
-}
-
-void UpdateChecker::updateCheckFinished(bool notifyNoUpdate)
-{
- qDebug() << "Finished downloading repo index. Checking for new versions.";
-
- QJsonParseError jsonError;
- indexJob.reset();
-
- QJsonDocument jsonDoc = QJsonDocument::fromJson(indexData, &jsonError);
- indexData.clear();
- if (jsonError.error != QJsonParseError::NoError || !jsonDoc.isObject())
- {
- qCritical() << "Failed to parse GoUpdate repository index. JSON error"
- << jsonError.errorString() << "at offset" << jsonError.offset;
- m_updateChecking = false;
- return;
- }
-
- QJsonObject object = jsonDoc.object();
-
- bool success = false;
- int apiVersion = object.value("ApiVersion").toVariant().toInt(&success);
- if (apiVersion != API_VERSION || !success)
- {
- qCritical() << "Failed to check for updates. API version mismatch. We're using"
- << API_VERSION << "server has" << apiVersion;
- m_updateChecking = false;
- return;
- }
-
- qDebug() << "Processing repository version list.";
- QJsonObject newestVersion;
- QJsonArray versions = object.value("Versions").toArray();
- for (QJsonValue versionVal : versions)
- {
- QJsonObject version = versionVal.toObject();
- if (newestVersion.value("Id").toVariant().toInt() <
- version.value("Id").toVariant().toInt())
- {
- newestVersion = version;
- }
- }
-
- // We've got the version with the greatest ID number. Now compare it to our current build
- // number and update if they're different.
- int newBuildNumber = newestVersion.value("Id").toVariant().toInt();
- if (newBuildNumber != m_currentBuild)
- {
- qDebug() << "Found newer version with ID" << newBuildNumber;
- // Update!
- GoUpdate::Status updateStatus;
- updateStatus.updateAvailable = true;
- updateStatus.currentVersionId = m_currentBuild;
- updateStatus.currentRepoUrl = m_currentRepoUrl;
- updateStatus.newVersionId = newBuildNumber;
- updateStatus.newRepoUrl = m_newRepoUrl;
- emit updateAvailable(updateStatus);
- }
- else if (notifyNoUpdate)
- {
- emit noUpdateFound();
- }
- m_updateChecking = false;
-}
-
-void UpdateChecker::updateCheckFailed()
-{
- qCritical() << "Update check failed for reasons unknown.";
-}
-
-void UpdateChecker::updateChanList(bool notifyNoUpdate)
-{
- qDebug() << "Loading the channel list.";
-
- if (m_chanListLoading)
- {
- qDebug() << "Ignoring channel list update request. Already grabbing channel list.";
- return;
- }
-
- m_chanListLoading = true;
- chanListJob = new NetJob("Update System Channel List", m_network);
- chanListJob->addNetAction(Net::Download::makeByteArray(QUrl(m_channelUrl), &chanlistData));
- connect(chanListJob.get(), &NetJob::succeeded, [this, notifyNoUpdate]() { chanListDownloadFinished(notifyNoUpdate); });
- connect(chanListJob.get(), &NetJob::failed, this, &UpdateChecker::chanListDownloadFailed);
- chanListJob->start();
-}
-
-void UpdateChecker::chanListDownloadFinished(bool notifyNoUpdate)
-{
- chanListJob.reset();
-
- QJsonParseError jsonError;
- QJsonDocument jsonDoc = QJsonDocument::fromJson(chanlistData, &jsonError);
- chanlistData.clear();
- if (jsonError.error != QJsonParseError::NoError)
- {
- // TODO: Report errors to the user.
- qCritical() << "Failed to parse channel list JSON:" << jsonError.errorString() << "at" << jsonError.offset;
- m_chanListLoading = false;
- return;
- }
-
- QJsonObject object = jsonDoc.object();
-
- bool success = false;
- int formatVersion = object.value("format_version").toVariant().toInt(&success);
- if (formatVersion != CHANLIST_FORMAT || !success)
- {
- qCritical()
- << "Failed to check for updates. Channel list format version mismatch. We're using"
- << CHANLIST_FORMAT << "server has" << formatVersion;
- m_chanListLoading = false;
- return;
- }
-
- // Load channels into a temporary array.
- QList<ChannelListEntry> loadedChannels;
- QJsonArray channelArray = object.value("channels").toArray();
- for (QJsonValue chanVal : channelArray)
- {
- QJsonObject channelObj = chanVal.toObject();
- ChannelListEntry entry {
- channelObj.value("id").toVariant().toString(),
- channelObj.value("name").toVariant().toString(),
- channelObj.value("description").toVariant().toString(),
- channelObj.value("url").toVariant().toString()
- };
- if (entry.id.isEmpty() || entry.name.isEmpty() || entry.url.isEmpty())
- {
- qCritical() << "Channel list entry with empty ID, name, or URL. Skipping.";
- continue;
- }
- loadedChannels.append(entry);
- }
-
- // Swap the channel list we just loaded into the object's channel list.
- m_channels.swap(loadedChannels);
-
- m_chanListLoading = false;
- m_chanListLoaded = true;
- qDebug() << "Successfully loaded UpdateChecker channel list.";
-
- // If we're waiting to check for updates, do that now.
- if (m_checkUpdateWaiting) {
- checkForUpdate(m_deferredUpdateChannel, notifyNoUpdate);
- }
-
- emit channelListLoaded();
-}
-
-void UpdateChecker::chanListDownloadFailed(QString reason)
-{
- m_chanListLoading = false;
- qCritical() << QString("Failed to download channel list: %1").arg(reason);
- emit channelListLoaded();
-}
-
diff --git a/launcher/updater/UpdateChecker.h b/launcher/updater/UpdateChecker.h
deleted file mode 100644
index 42ef318b..00000000
--- a/launcher/updater/UpdateChecker.h
+++ /dev/null
@@ -1,140 +0,0 @@
-/* Copyright 2013-2021 MultiMC Contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#pragma once
-
-#include "net/NetJob.h"
-#include "GoUpdate.h"
-#include "ExternalUpdater.h"
-
-#ifdef Q_OS_MAC
-#include "MacSparkleUpdater.h"
-#endif
-
-class UpdateChecker : public QObject
-{
- Q_OBJECT
-
-public:
- UpdateChecker(shared_qobject_ptr<QNetworkAccessManager> nam, QString channelUrl, QString currentChannel);
- void checkForUpdate(const QString& updateChannel, bool notifyNoUpdate);
-
- /*!
- * Causes the update checker to download the channel list from the URL specified in config.h (generated by CMake).
- * If this isn't called before checkForUpdate(), it will automatically be called.
- */
- void updateChanList(bool notifyNoUpdate);
-
- /*!
- * An entry in the channel list.
- */
- struct ChannelListEntry
- {
- QString id;
- QString name;
- QString description;
- QString url;
- };
-
- /*!
- * Returns a the current channel list.
- * If the channel list hasn't been loaded, this list will be empty.
- */
- QList<ChannelListEntry> getChannelList() const;
-
- /*!
- * Returns false if the channel list is empty.
- */
- bool hasChannels() const;
-
- /*!
- * Returns a pointer to an object that controls the external updater, or nullptr if an external updater is not used.
- */
- ExternalUpdater *getExternalUpdater();
-
-signals:
- //! Signal emitted when an update is available. Passes the URL for the repo and the ID and name for the version.
- void updateAvailable(GoUpdate::Status status);
-
- //! Signal emitted when the channel list finishes loading or fails to load.
- void channelListLoaded();
-
- void noUpdateFound();
-
-private slots:
- void updateCheckFinished(bool notifyNoUpdate);
- void updateCheckFailed();
-
- void chanListDownloadFinished(bool notifyNoUpdate);
- void chanListDownloadFailed(QString reason);
-
-private:
- friend class UpdateCheckerTest;
-
- shared_qobject_ptr<QNetworkAccessManager> m_network;
-
- NetJob::Ptr indexJob;
- QByteArray indexData;
- NetJob::Ptr chanListJob;
- QByteArray chanlistData;
-
- QString m_channelUrl;
-
- QList<ChannelListEntry> m_channels;
-
- /*!
- * True while the system is checking for updates.
- * If checkForUpdate is called while this is true, it will be ignored.
- */
- bool m_updateChecking = false;
-
- /*!
- * True if the channel list has loaded.
- * If this is false, trying to check for updates will call updateChanList first.
- */
- bool m_chanListLoaded = false;
-
- /*!
- * Set to true while the channel list is currently loading.
- */
- bool m_chanListLoading = false;
-
- /*!
- * Set to true when checkForUpdate is called while the channel list isn't loaded.
- * When the channel list finishes loading, if this is true, the update checker will check for updates.
- */
- bool m_checkUpdateWaiting = false;
-
- /*!
- * if m_checkUpdateWaiting, this is the last used update channel
- */
- QString m_deferredUpdateChannel;
-
- int m_currentBuild = -1;
- QString m_currentChannel;
- QString m_currentRepoUrl;
-
- QString m_newRepoUrl;
-
- /*!
- * If not a nullptr, then the updater here will be used instead of the old updater that uses GoUpdate when
- * checking for updates.
- *
- * As a result, signals from this class won't be emitted, and most of the functions in this class other
- * than checkForUpdate are not useful. Call functions from this external updater object instead.
- */
- ExternalUpdater *m_externalUpdater = nullptr;
-};
-
diff --git a/libraries/cmark b/libraries/cmark
-Subproject a8da5a2f252b96eca60ae8bada1a9ba059a3840
+Subproject 5ba25ff40eba44c811f79ab6a792baf945b8307
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 0f716a75..36a3b0f8 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -24,6 +24,9 @@ ecm_add_test(ResourceFolderModel_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_V
ecm_add_test(ResourcePackParse_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
TEST_NAME ResourcePackParse)
+ecm_add_test(ResourceModel_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
+ TEST_NAME ResourceModel)
+
ecm_add_test(TexturePackParse_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
TEST_NAME TexturePackParse)
diff --git a/tests/DummyResourceAPI.h b/tests/DummyResourceAPI.h
new file mode 100644
index 00000000..0cc90958
--- /dev/null
+++ b/tests/DummyResourceAPI.h
@@ -0,0 +1,46 @@
+#pragma once
+
+#include <QJsonDocument>
+
+#include <modplatform/ResourceAPI.h>
+
+class SearchTask : public Task {
+ Q_OBJECT
+
+ public:
+ void executeTask() override { emitSucceeded(); }
+};
+
+class DummyResourceAPI : public ResourceAPI {
+ public:
+ static auto searchRequestResult()
+ {
+ static QByteArray json_response =
+ "{\"hits\":["
+ "{"
+ "\"author\":\"flowln\","
+ "\"description\":\"the bestest mod\","
+ "\"project_id\":\"something\","
+ "\"project_type\":\"mod\","
+ "\"slug\":\"bip_bop\","
+ "\"title\":\"AAAAAAAA\","
+ "\"versions\":[\"2.71\"]"
+ "}"
+ "]}";
+
+ return QJsonDocument::fromJson(json_response);
+ }
+
+ DummyResourceAPI() : ResourceAPI() {}
+ [[nodiscard]] auto getSortingMethods() const -> QList<SortingMethod> override { return {}; };
+
+ [[nodiscard]] Task::Ptr searchProjects(SearchArgs&&, SearchCallbacks&& callbacks) const override
+ {
+ auto task = makeShared<SearchTask>();
+ QObject::connect(task.get(), &Task::succeeded, [=] {
+ auto json = searchRequestResult();
+ callbacks.on_succeed(json);
+ });
+ return task;
+ }
+};
diff --git a/tests/Packwiz_test.cpp b/tests/Packwiz_test.cpp
index 098e8f89..29289469 100644
--- a/tests/Packwiz_test.cpp
+++ b/tests/Packwiz_test.cpp
@@ -48,7 +48,7 @@ class PackwizTest : public QObject {
QCOMPARE(metadata.hash_format, "sha512");
QCOMPARE(metadata.hash, "c8fe6e15ddea32668822dddb26e1851e5f03834be4bcb2eff9c0da7fdc086a9b6cead78e31a44d3bc66335cba11144ee0337c6d5346f1ba63623064499b3188d");
- QCOMPARE(metadata.provider, ModPlatform::Provider::MODRINTH);
+ QCOMPARE(metadata.provider, ModPlatform::ResourceProvider::MODRINTH);
QCOMPARE(metadata.version(), "ug2qKTPR");
QCOMPARE(metadata.mod_id(), "kYq5qkSL");
}
@@ -76,7 +76,7 @@ class PackwizTest : public QObject {
QCOMPARE(metadata.hash_format, "murmur2");
QCOMPARE(metadata.hash, "1781245820");
- QCOMPARE(metadata.provider, ModPlatform::Provider::FLAME);
+ QCOMPARE(metadata.provider, ModPlatform::ResourceProvider::FLAME);
QCOMPARE(metadata.file_id, 3509043);
QCOMPARE(metadata.project_id, 327154);
}
diff --git a/tests/ResourceModel_test.cpp b/tests/ResourceModel_test.cpp
new file mode 100644
index 00000000..716bf853
--- /dev/null
+++ b/tests/ResourceModel_test.cpp
@@ -0,0 +1,88 @@
+#include <QAbstractItemModelTester>
+#include <QTest>
+#include <QTimer>
+
+#include <Json.h>
+
+#include <ui/pages/modplatform/ResourceModel.h>
+
+#include "DummyResourceAPI.h"
+
+using ResourceDownload::ResourceModel;
+
+#define EXEC_TASK(EXEC) \
+ QEventLoop loop; \
+ \
+ connect(model, &ResourceModel::dataChanged, &loop, &QEventLoop::quit); \
+ \
+ QTimer expire_timer; \
+ expire_timer.callOnTimeout(&loop, &QEventLoop::quit); \
+ expire_timer.setSingleShot(true); \
+ expire_timer.start(4000); \
+ \
+ EXEC; \
+ if (model->hasActiveSearchJob()) \
+ loop.exec(); \
+ \
+ QVERIFY2(expire_timer.isActive(), "Timer has expired. The search never finished."); \
+ expire_timer.stop(); \
+ \
+ disconnect(model, nullptr, &loop, nullptr)
+
+class ResourceModelTest;
+
+class DummyResourceModel : public ResourceModel {
+ Q_OBJECT
+
+ friend class ResourceModelTest;
+
+ public:
+ DummyResourceModel() : ResourceModel(new DummyResourceAPI) {}
+
+ [[nodiscard]] auto metaEntryBase() const -> QString override { return ""; };
+
+ ResourceAPI::SearchArgs createSearchArguments() override { return {}; };
+ ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) override { return {}; };
+ ResourceAPI::ProjectInfoArgs createInfoArguments(QModelIndex&) override { return {}; };
+
+ QJsonArray documentToArray(QJsonDocument& doc) const override { return doc.object().value("hits").toArray(); }
+
+ void loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) override
+ {
+ pack.authors.append({ Json::requireString(obj, "author") });
+ pack.description = Json::requireString(obj, "description");
+ pack.addonId = Json::requireString(obj, "project_id");
+ }
+};
+
+class ResourceModelTest : public QObject {
+ Q_OBJECT
+ private slots:
+ void test_abstract_item_model() { [[maybe_unused]] auto tester = new QAbstractItemModelTester(new DummyResourceModel); }
+
+ void test_search()
+ {
+ auto model = new DummyResourceModel;
+
+ QVERIFY(model->m_packs.isEmpty());
+
+ EXEC_TASK(model->search());
+
+ QVERIFY(model->m_packs.size() == 1);
+ QVERIFY(model->m_search_state == DummyResourceModel::SearchState::Finished);
+
+ auto processed_pack = model->m_packs.at(0);
+ auto search_json = DummyResourceAPI::searchRequestResult();
+ auto processed_response = model->documentToArray(search_json).first().toObject();
+
+ QVERIFY(processed_pack.addonId.toString() == Json::requireString(processed_response, "project_id"));
+ QVERIFY(processed_pack.description == Json::requireString(processed_response, "description"));
+ QVERIFY(processed_pack.authors.first().name == Json::requireString(processed_response, "author"));
+ }
+};
+
+QTEST_GUILESS_MAIN(ResourceModelTest)
+
+#include "ResourceModel_test.moc"
+
+#include "moc_DummyResourceAPI.cpp"
diff --git a/tests/Task_test.cpp b/tests/Task_test.cpp
index 80bba02f..95eb4a30 100644
--- a/tests/Task_test.cpp
+++ b/tests/Task_test.cpp
@@ -1,16 +1,23 @@
#include <QTest>
+#include <QTimer>
+#include <QThread>
#include <tasks/ConcurrentTask.h>
#include <tasks/MultipleOptionsTask.h>
#include <tasks/SequentialTask.h>
#include <tasks/Task.h>
+#include <array>
+
/* Does nothing. Only used for testing. */
class BasicTask : public Task {
Q_OBJECT
friend class TaskTest;
+ public:
+ BasicTask(bool show_debug_log = true) : Task(nullptr, show_debug_log) {}
+
private:
void executeTask() override
{
@@ -30,6 +37,57 @@ class BasicTask_MultiStep : public Task {
void executeTask() override {};
};
+class BigConcurrentTask : public ConcurrentTask {
+ Q_OBJECT
+
+ void startNext() override
+ {
+ // This is here only to help fill the stack a bit more quickly (if there's an issue, of course :^))
+ // Each tasks thus adds 1024 * 4 bytes to the stack, at the very least.
+ [[maybe_unused]] volatile std::array<uint32_t, 1024> some_data_on_the_stack {};
+
+ ConcurrentTask::startNext();
+ }
+};
+
+class BigConcurrentTaskThread : public QThread {
+ Q_OBJECT
+
+ BigConcurrentTask big_task;
+
+ void run() override
+ {
+ QTimer deadline;
+ deadline.setInterval(10000);
+ connect(&deadline, &QTimer::timeout, this, [this]{ passed_the_deadline = true; });
+ deadline.start();
+
+ // NOTE: Arbitrary value that manages to trigger a problem when there is one.
+ // Considering each tasks, in a problematic state, adds 1024 * 4 bytes to the stack,
+ // this number is enough to fill up 16 MiB of stack, more than enough to cause a problem.
+ static const unsigned s_num_tasks = 1 << 12;
+ auto sub_tasks = new BasicTask::Ptr[s_num_tasks];
+
+ for (unsigned i = 0; i < s_num_tasks; i++) {
+ sub_tasks[i] = makeShared<BasicTask>(false);
+ big_task.addTask(sub_tasks[i]);
+ }
+
+ big_task.run();
+
+ while (!big_task.isFinished() && !passed_the_deadline)
+ QCoreApplication::processEvents();
+
+ emit finished();
+ }
+
+ public:
+ bool passed_the_deadline = false;
+
+ signals:
+ void finished();
+};
+
class TaskTest : public QObject {
Q_OBJECT
@@ -78,21 +136,21 @@ class TaskTest : public QObject {
}
void test_basicConcurrentRun(){
- BasicTask t1;
- BasicTask t2;
- BasicTask t3;
+ auto t1 = makeShared<BasicTask>();
+ auto t2 = makeShared<BasicTask>();
+ auto t3 = makeShared<BasicTask>();
ConcurrentTask t;
- t.addTask(&t1);
- t.addTask(&t2);
- t.addTask(&t3);
+ t.addTask(t1);
+ t.addTask(t2);
+ t.addTask(t3);
QObject::connect(&t, &Task::finished, [&]{
QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been.");
- QVERIFY(t1.wasSuccessful());
- QVERIFY(t2.wasSuccessful());
- QVERIFY(t3.wasSuccessful());
+ QVERIFY(t1->wasSuccessful());
+ QVERIFY(t2->wasSuccessful());
+ QVERIFY(t3->wasSuccessful());
});
t.start();
@@ -103,31 +161,39 @@ class TaskTest : public QObject {
// Tests if starting new tasks after the 6 initial ones is working
void test_moreConcurrentRun(){
- BasicTask t1, t2, t3, t4, t5, t6, t7, t8, t9;
+ auto t1 = makeShared<BasicTask>();
+ auto t2 = makeShared<BasicTask>();
+ auto t3 = makeShared<BasicTask>();
+ auto t4 = makeShared<BasicTask>();
+ auto t5 = makeShared<BasicTask>();
+ auto t6 = makeShared<BasicTask>();
+ auto t7 = makeShared<BasicTask>();
+ auto t8 = makeShared<BasicTask>();
+ auto t9 = makeShared<BasicTask>();
ConcurrentTask t;
- t.addTask(&t1);
- t.addTask(&t2);
- t.addTask(&t3);
- t.addTask(&t4);
- t.addTask(&t5);
- t.addTask(&t6);
- t.addTask(&t7);
- t.addTask(&t8);
- t.addTask(&t9);
+ t.addTask(t1);
+ t.addTask(t2);
+ t.addTask(t3);
+ t.addTask(t4);
+ t.addTask(t5);
+ t.addTask(t6);
+ t.addTask(t7);
+ t.addTask(t8);
+ t.addTask(t9);
QObject::connect(&t, &Task::finished, [&]{
QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been.");
- QVERIFY(t1.wasSuccessful());
- QVERIFY(t2.wasSuccessful());
- QVERIFY(t3.wasSuccessful());
- QVERIFY(t4.wasSuccessful());
- QVERIFY(t5.wasSuccessful());
- QVERIFY(t6.wasSuccessful());
- QVERIFY(t7.wasSuccessful());
- QVERIFY(t8.wasSuccessful());
- QVERIFY(t9.wasSuccessful());
+ QVERIFY(t1->wasSuccessful());
+ QVERIFY(t2->wasSuccessful());
+ QVERIFY(t3->wasSuccessful());
+ QVERIFY(t4->wasSuccessful());
+ QVERIFY(t5->wasSuccessful());
+ QVERIFY(t6->wasSuccessful());
+ QVERIFY(t7->wasSuccessful());
+ QVERIFY(t8->wasSuccessful());
+ QVERIFY(t9->wasSuccessful());
});
t.start();
@@ -137,21 +203,21 @@ class TaskTest : public QObject {
}
void test_basicSequentialRun(){
- BasicTask t1;
- BasicTask t2;
- BasicTask t3;
+ auto t1 = makeShared<BasicTask>();
+ auto t2 = makeShared<BasicTask>();
+ auto t3 = makeShared<BasicTask>();
SequentialTask t;
- t.addTask(&t1);
- t.addTask(&t2);
- t.addTask(&t3);
+ t.addTask(t1);
+ t.addTask(t2);
+ t.addTask(t3);
QObject::connect(&t, &Task::finished, [&]{
QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been.");
- QVERIFY(t1.wasSuccessful());
- QVERIFY(t2.wasSuccessful());
- QVERIFY(t3.wasSuccessful());
+ QVERIFY(t1->wasSuccessful());
+ QVERIFY(t2->wasSuccessful());
+ QVERIFY(t3->wasSuccessful());
});
t.start();
@@ -161,21 +227,21 @@ class TaskTest : public QObject {
}
void test_basicMultipleOptionsRun(){
- BasicTask t1;
- BasicTask t2;
- BasicTask t3;
+ auto t1 = makeShared<BasicTask>();
+ auto t2 = makeShared<BasicTask>();
+ auto t3 = makeShared<BasicTask>();
MultipleOptionsTask t;
- t.addTask(&t1);
- t.addTask(&t2);
- t.addTask(&t3);
+ t.addTask(t1);
+ t.addTask(t2);
+ t.addTask(t3);
QObject::connect(&t, &Task::finished, [&]{
QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been.");
- QVERIFY(t1.wasSuccessful());
- QVERIFY(!t2.wasSuccessful());
- QVERIFY(!t3.wasSuccessful());
+ QVERIFY(t1->wasSuccessful());
+ QVERIFY(!t2->wasSuccessful());
+ QVERIFY(!t3->wasSuccessful());
});
t.start();
@@ -183,6 +249,22 @@ class TaskTest : public QObject {
return t.isFinished();
}, 1000), "Task didn't finish as it should.");
}
+
+ void test_stackOverflowInConcurrentTask()
+ {
+ QEventLoop loop;
+
+ auto thread = new BigConcurrentTaskThread;
+
+ connect(thread, &BigConcurrentTaskThread::finished, &loop, &QEventLoop::quit);
+
+ thread->start();
+
+ loop.exec();
+
+ QVERIFY(!thread->passed_the_deadline);
+ thread->deleteLater();
+ }
};
QTEST_GUILESS_MAIN(TaskTest)