aboutsummaryrefslogtreecommitdiff
path: root/launcher
diff options
context:
space:
mode:
Diffstat (limited to 'launcher')
-rw-r--r--launcher/Application.cpp113
-rw-r--r--launcher/Application.h5
-rw-r--r--launcher/BaseInstance.cpp36
-rw-r--r--launcher/BaseInstance.h6
-rw-r--r--launcher/CMakeLists.txt152
-rw-r--r--launcher/DesktopServices.cpp1
-rw-r--r--launcher/FastFileIconProvider.cpp47
-rw-r--r--launcher/FastFileIconProvider.h26
-rw-r--r--launcher/FileIgnoreProxy.cpp279
-rw-r--r--launcher/FileIgnoreProxy.h85
-rw-r--r--launcher/FileSystem.cpp1041
-rw-r--r--launcher/FileSystem.h356
-rw-r--r--launcher/InstanceCopyPrefs.cpp59
-rw-r--r--launcher/InstanceCopyPrefs.h16
-rw-r--r--launcher/InstanceCopyTask.cpp131
-rw-r--r--launcher/InstanceCopyTask.h5
-rw-r--r--launcher/InstanceCreationTask.h2
-rw-r--r--launcher/InstanceImportTask.cpp27
-rw-r--r--launcher/InstanceList.cpp14
-rw-r--r--launcher/InstanceList.h2
-rw-r--r--launcher/InstanceTask.cpp5
-rw-r--r--launcher/JavaCommon.cpp6
-rw-r--r--launcher/LaunchController.cpp4
-rw-r--r--launcher/LoggedProcess.cpp8
-rw-r--r--launcher/MMCTime.cpp64
-rw-r--r--launcher/MMCTime.h9
-rw-r--r--launcher/MMCZip.cpp16
-rw-r--r--launcher/MMCZip.h6
-rw-r--r--launcher/MTPixmapCache.h41
-rw-r--r--launcher/Markdown.cpp31
-rw-r--r--launcher/Markdown.h12
-rw-r--r--launcher/QVariantUtils.h70
-rw-r--r--launcher/ResourceDownloadTask.cpp50
-rw-r--r--launcher/ResourceDownloadTask.h60
-rw-r--r--launcher/StringUtils.cpp108
-rw-r--r--launcher/StringUtils.h50
-rw-r--r--launcher/VersionProxyModel.cpp59
-rw-r--r--launcher/VersionProxyModel.h3
-rw-r--r--launcher/filelink/FileLink.cpp277
-rw-r--r--launcher/filelink/FileLink.h67
-rw-r--r--launcher/filelink/filelink.exe.manifest28
-rw-r--r--launcher/filelink/main.cpp30
-rw-r--r--launcher/icons/IconList.cpp5
-rw-r--r--launcher/java/JavaChecker.cpp18
-rw-r--r--launcher/java/JavaCheckerJob.cpp2
-rw-r--r--launcher/java/JavaInstallList.cpp5
-rw-r--r--launcher/launch/steps/CheckJava.cpp15
-rw-r--r--launcher/launch/steps/CheckJava.h2
-rw-r--r--launcher/launch/steps/Update.cpp8
-rw-r--r--launcher/meta/Index.cpp8
-rw-r--r--launcher/meta/JsonFormat.cpp7
-rw-r--r--launcher/meta/Version.cpp4
-rw-r--r--launcher/meta/Version.h4
-rw-r--r--launcher/meta/VersionList.cpp4
-rw-r--r--launcher/minecraft/Component.cpp4
-rw-r--r--launcher/minecraft/MinecraftInstance.cpp55
-rw-r--r--launcher/minecraft/MinecraftInstance.h16
-rw-r--r--launcher/minecraft/MinecraftLoadAndCheck.cpp1
-rw-r--r--launcher/minecraft/MinecraftUpdate.cpp4
-rw-r--r--launcher/minecraft/OneSixVersionFormat.cpp10
-rw-r--r--launcher/minecraft/PackProfile.cpp3
-rw-r--r--launcher/minecraft/VersionFile.h2
-rw-r--r--launcher/minecraft/World.cpp24
-rw-r--r--launcher/minecraft/World.h15
-rw-r--r--launcher/minecraft/WorldList.cpp37
-rw-r--r--launcher/minecraft/WorldList.h9
-rw-r--r--launcher/minecraft/auth/AccountList.cpp18
-rw-r--r--launcher/minecraft/auth/AuthRequest.cpp22
-rw-r--r--launcher/minecraft/auth/MinecraftAccount.cpp16
-rw-r--r--launcher/minecraft/auth/Yggdrasil.cpp1
-rw-r--r--launcher/minecraft/auth/steps/XboxUserStep.cpp4
-rw-r--r--launcher/minecraft/mod/DataPack.cpp6
-rw-r--r--launcher/minecraft/mod/Mod.cpp71
-rw-r--r--launcher/minecraft/mod/Mod.h25
-rw-r--r--launcher/minecraft/mod/ModDetails.h97
-rw-r--r--launcher/minecraft/mod/ModFolderModel.cpp59
-rw-r--r--launcher/minecraft/mod/ModFolderModel.h5
-rw-r--r--launcher/minecraft/mod/Resource.cpp25
-rw-r--r--launcher/minecraft/mod/Resource.h15
-rw-r--r--launcher/minecraft/mod/ResourceFolderModel.cpp183
-rw-r--r--launcher/minecraft/mod/ResourceFolderModel.h34
-rw-r--r--launcher/minecraft/mod/ResourcePack.cpp24
-rw-r--r--launcher/minecraft/mod/ResourcePack.h6
-rw-r--r--launcher/minecraft/mod/ResourcePackFolderModel.cpp47
-rw-r--r--launcher/minecraft/mod/ResourcePackFolderModel.h5
-rw-r--r--launcher/minecraft/mod/ShaderPackFolderModel.h6
-rw-r--r--launcher/minecraft/mod/TexturePack.cpp25
-rw-r--r--launcher/minecraft/mod/TexturePack.h6
-rw-r--r--launcher/minecraft/mod/TexturePackFolderModel.cpp106
-rw-r--r--launcher/minecraft/mod/TexturePackFolderModel.h25
-rw-r--r--launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp252
-rw-r--r--launcher/minecraft/mod/tasks/GetModDependenciesTask.h84
-rw-r--r--launcher/minecraft/mod/tasks/LocalModParseTask.cpp217
-rw-r--r--launcher/minecraft/mod/tasks/LocalModParseTask.h3
-rw-r--r--launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp33
-rw-r--r--launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp67
-rw-r--r--launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h5
-rw-r--r--launcher/minecraft/mod/tasks/LocalResourceParse.cpp9
-rw-r--r--launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp67
-rw-r--r--launcher/minecraft/mod/tasks/LocalTexturePackParseTask.h5
-rw-r--r--launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp2
-rw-r--r--launcher/minecraft/services/CapeChange.cpp33
-rw-r--r--launcher/minecraft/services/CapeChange.h1
-rw-r--r--launcher/minecraft/services/SkinDelete.cpp22
-rw-r--r--launcher/minecraft/services/SkinDelete.h1
-rw-r--r--launcher/minecraft/services/SkinUpload.cpp22
-rw-r--r--launcher/minecraft/services/SkinUpload.h1
-rw-r--r--launcher/minecraft/update/AssetUpdateTask.cpp2
-rw-r--r--launcher/minecraft/update/FMLLibrariesTask.cpp1
-rw-r--r--launcher/minecraft/update/LibrariesTask.cpp2
-rw-r--r--launcher/modplatform/EnsureMetadataTask.cpp31
-rw-r--r--launcher/modplatform/ModIndex.cpp8
-rw-r--r--launcher/modplatform/ModIndex.h66
-rw-r--r--launcher/modplatform/ResourceAPI.h20
-rw-r--r--launcher/modplatform/atlauncher/ATLPackInstallTask.cpp22
-rw-r--r--launcher/modplatform/atlauncher/ATLPackInstallTask.h49
-rw-r--r--launcher/modplatform/flame/FileResolvingTask.cpp153
-rw-r--r--launcher/modplatform/flame/FileResolvingTask.h36
-rw-r--r--launcher/modplatform/flame/FlameAPI.cpp51
-rw-r--r--launcher/modplatform/flame/FlameAPI.h56
-rw-r--r--launcher/modplatform/flame/FlameCheckUpdate.cpp26
-rw-r--r--launcher/modplatform/flame/FlameInstanceCreationTask.cpp28
-rw-r--r--launcher/modplatform/flame/FlameModIndex.cpp81
-rw-r--r--launcher/modplatform/flame/FlameModIndex.h6
-rw-r--r--launcher/modplatform/flame/FlamePackExportTask.cpp473
-rw-r--r--launcher/modplatform/flame/FlamePackExportTask.h90
-rw-r--r--launcher/modplatform/flame/FlamePackIndex.h10
-rw-r--r--launcher/modplatform/flame/PackManifest.cpp9
-rw-r--r--launcher/modplatform/helpers/ExportToModList.cpp200
-rw-r--r--launcher/modplatform/helpers/ExportToModList.h33
-rw-r--r--launcher/modplatform/helpers/HashUtils.cpp16
-rw-r--r--launcher/modplatform/helpers/HashUtils.h5
-rw-r--r--launcher/modplatform/helpers/NetworkResourceAPI.cpp56
-rw-r--r--launcher/modplatform/helpers/NetworkResourceAPI.h5
-rw-r--r--launcher/modplatform/legacy_ftb/PackFetchTask.cpp68
-rw-r--r--launcher/modplatform/legacy_ftb/PackFetchTask.h26
-rw-r--r--launcher/modplatform/legacy_ftb/PackInstallTask.cpp87
-rw-r--r--launcher/modplatform/legacy_ftb/PackInstallTask.h27
-rw-r--r--launcher/modplatform/modpacksch/FTBPackInstallTask.cpp387
-rw-r--r--launcher/modplatform/modpacksch/FTBPackInstallTask.h101
-rw-r--r--launcher/modplatform/modpacksch/FTBPackManifest.cpp195
-rw-r--r--launcher/modplatform/modpacksch/FTBPackManifest.h168
-rw-r--r--launcher/modplatform/modrinth/ModrinthAPI.cpp22
-rw-r--r--launcher/modplatform/modrinth/ModrinthAPI.h43
-rw-r--r--launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp33
-rw-r--r--launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp11
-rw-r--r--launcher/modplatform/modrinth/ModrinthPackExportTask.cpp332
-rw-r--r--launcher/modplatform/modrinth/ModrinthPackExportTask.h77
-rw-r--r--launcher/modplatform/modrinth/ModrinthPackIndex.cpp43
-rw-r--r--launcher/modplatform/modrinth/ModrinthPackIndex.h3
-rw-r--r--launcher/modplatform/technic/SingleZipPackInstallTask.cpp1
-rw-r--r--launcher/modplatform/technic/SolderPackInstallTask.cpp34
-rw-r--r--launcher/modplatform/technic/SolderPackInstallTask.h73
-rw-r--r--launcher/mojang/PackageManifest.cpp427
-rw-r--r--launcher/mojang/PackageManifest.h171
-rw-r--r--launcher/net/ByteArraySink.h22
-rw-r--r--launcher/net/Download.cpp100
-rw-r--r--launcher/net/Download.h14
-rw-r--r--launcher/net/FileSink.cpp12
-rw-r--r--launcher/net/FileSink.h2
-rw-r--r--launcher/net/HttpMetaCache.cpp20
-rw-r--r--launcher/net/HttpMetaCache.h2
-rw-r--r--launcher/net/Logging.cpp26
-rw-r--r--launcher/net/Logging.h28
-rw-r--r--launcher/net/MetaCacheSink.cpp12
-rw-r--r--launcher/net/MetaCacheSink.h2
-rw-r--r--launcher/net/NetAction.h18
-rw-r--r--launcher/net/NetJob.cpp3
-rw-r--r--launcher/net/NetJob.h3
-rw-r--r--launcher/net/PasteUpload.cpp24
-rw-r--r--launcher/net/PasteUpload.h2
-rw-r--r--launcher/net/Upload.cpp369
-rw-r--r--launcher/net/Upload.h45
-rw-r--r--launcher/news/NewsChecker.cpp19
-rw-r--r--launcher/news/NewsChecker.h2
-rw-r--r--launcher/qtlogging.ini16
-rw-r--r--launcher/resources/OSX/OSX.qrc1
-rw-r--r--launcher/resources/OSX/scalable/launcher.svg57
-rw-r--r--launcher/resources/breeze_dark/scalable/launcher.svg57
-rw-r--r--launcher/resources/flat/flat.qrc1
-rw-r--r--launcher/resources/flat/scalable/launcher.svg57
-rw-r--r--launcher/resources/flat_white/flat_white.qrc1
-rw-r--r--launcher/resources/flat_white/scalable/launcher.svg2
-rw-r--r--launcher/resources/iOS/iOS.qrc1
-rw-r--r--launcher/resources/iOS/scalable/launcher.svg57
-rw-r--r--launcher/resources/pe_blue/pe_blue.qrc1
-rw-r--r--launcher/resources/pe_blue/scalable/launcher.svg57
-rw-r--r--launcher/resources/pe_colored/pe_colored.qrc1
-rw-r--r--launcher/resources/pe_colored/scalable/launcher.svg57
-rw-r--r--launcher/resources/pe_dark/pe_dark.qrc1
-rw-r--r--launcher/resources/pe_dark/scalable/launcher.svg57
-rw-r--r--launcher/resources/pe_light/pe_light.qrc1
-rw-r--r--launcher/resources/pe_light/scalable/launcher.svg57
-rw-r--r--launcher/screenshots/ImgurAlbumCreation.cpp10
-rw-r--r--launcher/screenshots/ImgurUpload.cpp10
-rw-r--r--launcher/settings/INIFile.cpp168
-rw-r--r--launcher/settings/INIFile.h47
-rw-r--r--launcher/settings/SettingsObject.cpp9
-rw-r--r--launcher/settings/SettingsObject.h2
-rw-r--r--launcher/tasks/ConcurrentTask.cpp195
-rw-r--r--launcher/tasks/ConcurrentTask.h70
-rw-r--r--launcher/tasks/MultipleOptionsTask.cpp34
-rw-r--r--launcher/tasks/MultipleOptionsTask.h34
-rw-r--r--launcher/tasks/SequentialTask.cpp35
-rw-r--r--launcher/tasks/SequentialTask.h35
-rw-r--r--launcher/tasks/Task.cpp52
-rw-r--r--launcher/tasks/Task.h66
-rw-r--r--launcher/tools/JProfiler.cpp4
-rw-r--r--launcher/tools/JVisualVM.cpp4
-rw-r--r--launcher/translations/TranslationsModel.cpp38
-rw-r--r--launcher/translations/TranslationsModel.h29
-rw-r--r--launcher/ui/MainWindow.cpp923
-rw-r--r--launcher/ui/MainWindow.h11
-rw-r--r--launcher/ui/MainWindow.ui50
-rw-r--r--launcher/ui/dialogs/AboutDialog.cpp11
-rw-r--r--launcher/ui/dialogs/BlockedModsDialog.cpp39
-rw-r--r--launcher/ui/dialogs/CopyInstanceDialog.cpp125
-rw-r--r--launcher/ui/dialogs/CopyInstanceDialog.h33
-rw-r--r--launcher/ui/dialogs/CopyInstanceDialog.ui358
-rw-r--r--launcher/ui/dialogs/ExportInstanceDialog.cpp333
-rw-r--r--launcher/ui/dialogs/ExportInstanceDialog.h46
-rw-r--r--launcher/ui/dialogs/ExportPackDialog.cpp146
-rw-r--r--launcher/ui/dialogs/ExportPackDialog.h49
-rw-r--r--launcher/ui/dialogs/ExportPackDialog.ui137
-rw-r--r--launcher/ui/dialogs/ExportToModListDialog.cpp223
-rw-r--r--launcher/ui/dialogs/ExportToModListDialog.h55
-rw-r--r--launcher/ui/dialogs/ExportToModListDialog.ui240
-rw-r--r--launcher/ui/dialogs/NewInstanceDialog.cpp8
-rw-r--r--launcher/ui/dialogs/NewsDialog.cpp2
-rw-r--r--launcher/ui/dialogs/ProgressDialog.cpp160
-rw-r--r--launcher/ui/dialogs/ProgressDialog.h56
-rw-r--r--launcher/ui/dialogs/ProgressDialog.ui150
-rw-r--r--launcher/ui/dialogs/ResourceDownloadDialog.cpp171
-rw-r--r--launcher/ui/dialogs/ResourceDownloadDialog.h21
-rw-r--r--launcher/ui/dialogs/ReviewMessageBox.cpp29
-rw-r--r--launcher/ui/dialogs/ReviewMessageBox.h8
-rw-r--r--launcher/ui/dialogs/VersionSelectDialog.cpp47
-rw-r--r--launcher/ui/instanceview/AccessibleInstanceView.cpp2
-rw-r--r--launcher/ui/instanceview/InstanceView.cpp26
-rw-r--r--launcher/ui/instanceview/InstanceView.h8
-rw-r--r--launcher/ui/pages/BasePage.h24
-rw-r--r--launcher/ui/pages/global/APIPage.cpp6
-rw-r--r--launcher/ui/pages/global/LauncherPage.ui10
-rw-r--r--launcher/ui/pages/global/MinecraftPage.cpp1
-rw-r--r--launcher/ui/pages/global/MinecraftPage.ui135
-rw-r--r--launcher/ui/pages/instance/ExternalResourcesPage.cpp104
-rw-r--r--launcher/ui/pages/instance/ExternalResourcesPage.h4
-rw-r--r--launcher/ui/pages/instance/ExternalResourcesPage.ui14
-rw-r--r--launcher/ui/pages/instance/InstanceSettingsPage.cpp65
-rw-r--r--launcher/ui/pages/instance/InstanceSettingsPage.h6
-rw-r--r--launcher/ui/pages/instance/InstanceSettingsPage.ui17
-rw-r--r--launcher/ui/pages/instance/ManagedPackPage.cpp66
-rw-r--r--launcher/ui/pages/instance/ModFolderPage.cpp81
-rw-r--r--launcher/ui/pages/instance/ModFolderPage.h5
-rw-r--r--launcher/ui/pages/instance/ResourcePackPage.cpp2
-rw-r--r--launcher/ui/pages/instance/ScreenshotsPage.cpp79
-rw-r--r--launcher/ui/pages/instance/ShaderPackPage.cpp3
-rw-r--r--launcher/ui/pages/instance/TexturePackPage.cpp2
-rw-r--r--launcher/ui/pages/instance/VersionPage.cpp311
-rw-r--r--launcher/ui/pages/instance/VersionPage.h55
-rw-r--r--launcher/ui/pages/instance/VersionPage.ui10
-rw-r--r--launcher/ui/pages/instance/WorldListPage.cpp2
-rw-r--r--launcher/ui/pages/modplatform/CustomPage.cpp (renamed from launcher/ui/pages/modplatform/VanillaPage.cpp)68
-rw-r--r--launcher/ui/pages/modplatform/CustomPage.h (renamed from launcher/ui/pages/modplatform/VanillaPage.h)12
-rw-r--r--launcher/ui/pages/modplatform/CustomPage.ui (renamed from launcher/ui/pages/modplatform/VanillaPage.ui)4
-rw-r--r--launcher/ui/pages/modplatform/ImportPage.h2
-rw-r--r--launcher/ui/pages/modplatform/ModModel.cpp20
-rw-r--r--launcher/ui/pages/modplatform/ModModel.h6
-rw-r--r--launcher/ui/pages/modplatform/ModPage.cpp43
-rw-r--r--launcher/ui/pages/modplatform/ModPage.h17
-rw-r--r--launcher/ui/pages/modplatform/ResourceModel.cpp91
-rw-r--r--launcher/ui/pages/modplatform/ResourceModel.h16
-rw-r--r--launcher/ui/pages/modplatform/ResourcePackModel.cpp4
-rw-r--r--launcher/ui/pages/modplatform/ResourcePackPage.h2
-rw-r--r--launcher/ui/pages/modplatform/ResourcePage.cpp108
-rw-r--r--launcher/ui/pages/modplatform/ResourcePage.h18
-rw-r--r--launcher/ui/pages/modplatform/ShaderPackModel.cpp4
-rw-r--r--launcher/ui/pages/modplatform/ShaderPackPage.cpp16
-rw-r--r--launcher/ui/pages/modplatform/ShaderPackPage.h6
-rw-r--r--launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp99
-rw-r--r--launcher/ui/pages/modplatform/atlauncher/AtlListModel.h29
-rw-r--r--launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp8
-rw-r--r--launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h4
-rw-r--r--launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.cpp2
-rw-r--r--launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.h8
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameModel.cpp8
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameModel.h53
-rw-r--r--launcher/ui/pages/modplatform/flame/FlamePage.cpp7
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp9
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameResourceModels.h3
-rw-r--r--launcher/ui/pages/modplatform/ftb/FtbFilterModel.cpp93
-rw-r--r--launcher/ui/pages/modplatform/ftb/FtbFilterModel.h51
-rw-r--r--launcher/ui/pages/modplatform/ftb/FtbListModel.cpp304
-rw-r--r--launcher/ui/pages/modplatform/ftb/FtbListModel.h83
-rw-r--r--launcher/ui/pages/modplatform/ftb/FtbPage.cpp199
-rw-r--r--launcher/ui/pages/modplatform/ftb/FtbPage.h105
-rw-r--r--launcher/ui/pages/modplatform/ftb/FtbPage.ui86
-rw-r--r--launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp111
-rw-r--r--launcher/ui/pages/modplatform/legacy_ftb/Page.cpp4
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp20
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthModel.h4
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp20
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp13
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h3
-rw-r--r--launcher/ui/pages/modplatform/technic/TechnicModel.cpp118
-rw-r--r--launcher/ui/pages/modplatform/technic/TechnicModel.h27
-rw-r--r--launcher/ui/pages/modplatform/technic/TechnicPage.cpp15
-rw-r--r--launcher/ui/pages/modplatform/technic/TechnicPage.h2
-rw-r--r--launcher/ui/setupwizard/JavaWizardPage.cpp1
-rw-r--r--launcher/ui/setupwizard/SetupWizard.cpp2
-rw-r--r--launcher/ui/themes/SystemTheme.cpp2
-rw-r--r--launcher/ui/widgets/InfoFrame.cpp267
-rw-r--r--launcher/ui/widgets/InfoFrame.h47
-rw-r--r--launcher/ui/widgets/InfoFrame.ui92
-rw-r--r--launcher/ui/widgets/JavaSettingsWidget.cpp2
-rw-r--r--launcher/ui/widgets/LanguageSelectionWidget.cpp28
-rw-r--r--launcher/ui/widgets/LanguageSelectionWidget.h25
-rw-r--r--launcher/ui/widgets/ModListView.cpp21
-rw-r--r--launcher/ui/widgets/ModListView.h2
-rw-r--r--launcher/ui/widgets/PageContainer.cpp13
-rw-r--r--launcher/ui/widgets/PageContainer.h1
-rw-r--r--launcher/ui/widgets/ProgressWidget.cpp1
-rw-r--r--launcher/ui/widgets/ProjectItem.cpp11
-rw-r--r--launcher/ui/widgets/ProjectItem.h3
-rw-r--r--launcher/ui/widgets/SubTaskProgressBar.cpp58
-rw-r--r--launcher/ui/widgets/SubTaskProgressBar.h48
-rw-r--r--launcher/ui/widgets/SubTaskProgressBar.ui94
-rw-r--r--launcher/ui/widgets/VersionListView.cpp8
-rw-r--r--launcher/ui/widgets/VersionSelectWidget.cpp46
-rw-r--r--launcher/ui/widgets/VersionSelectWidget.h48
-rw-r--r--launcher/ui/widgets/WideBar.cpp13
331 files changed, 11848 insertions, 6616 deletions
diff --git a/launcher/Application.cpp b/launcher/Application.cpp
index a7c97aa7..e6070006 100644
--- a/launcher/Application.cpp
+++ b/launcher/Application.cpp
@@ -8,6 +8,7 @@
* Copyright (C) 2022 Lenny McLennington <lenny@sneed.church>
* Copyright (C) 2022 Tayou <tayou@gmx.net>
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
+ * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.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
@@ -46,6 +47,7 @@
#include "net/PasteUpload.h"
#include "pathmatcher/MultiMatcher.h"
#include "pathmatcher/SimplePrefixMatcher.h"
+#include "settings/INIFile.h"
#include "ui/MainWindow.h"
#include "ui/InstanceWindow.h"
@@ -286,6 +288,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
if (QFile::exists(FS::PathCombine(m_rootPath, "portable.txt"))) {
dataPath = m_rootPath;
adjustedBy = "Portable data path";
+ m_portable = true;
}
#endif
}
@@ -373,33 +376,33 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
// init the logger
{
- static const QString logBase = BuildConfig.LAUNCHER_NAME + "-%0.log";
- auto moveFile = [](const QString &oldName, const QString &newName)
- {
+ static const QString baseLogFile = BuildConfig.LAUNCHER_NAME + "-%0.log";
+ static const QString logBase = FS::PathCombine("logs", baseLogFile);
+ auto moveFile = [](const QString& oldName, const QString& newName) {
QFile::remove(newName);
QFile::copy(oldName, newName);
QFile::remove(oldName);
};
+ if (FS::ensureFolderPathExists("logs")) { // if this did not fail
+ for (auto i = 0; i <= 4; i++)
+ if (auto oldName = baseLogFile.arg(i);
+ QFile::exists(oldName)) // do not pointlessly delete new files if the old ones are not there
+ moveFile(oldName, logBase.arg(i));
+ }
- moveFile(logBase.arg(3), logBase.arg(4));
- moveFile(logBase.arg(2), logBase.arg(3));
- moveFile(logBase.arg(1), logBase.arg(2));
- moveFile(logBase.arg(0), logBase.arg(1));
+ for (auto i = 4; i > 0; i--)
+ moveFile(logBase.arg(i - 1), logBase.arg(i));
logFile = std::unique_ptr<QFile>(new QFile(logBase.arg(0)));
- if(!logFile->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate))
- {
- showFatalErrorMessage(
- "The launcher data folder is not writable!",
- QString(
- "The launcher couldn't create a log file - the data folder is not writable.\n"
- "\n"
- "Make sure you have write permissions to the data folder.\n"
- "(%1)\n"
- "\n"
- "The launcher cannot continue until you fix this problem."
- ).arg(dataPath)
- );
+ if (!logFile->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) {
+ showFatalErrorMessage("The launcher data folder is not writable!",
+ QString("The launcher couldn't create a log file - the data folder is not writable.\n"
+ "\n"
+ "Make sure you have write permissions to the data folder.\n"
+ "(%1)\n"
+ "\n"
+ "The launcher cannot continue until you fix this problem.")
+ .arg(dataPath));
return;
}
qInstallMessageHandler(appDebugOutput);
@@ -410,6 +413,51 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
" " "|" " "
"%{if-category}[%{category}]: %{endif}"
"%{message}");
+
+ bool foundLoggingRules = false;
+
+ auto logRulesFile = QStringLiteral("qtlogging.ini");
+ auto logRulesPath = FS::PathCombine(dataPath, logRulesFile);
+
+ qDebug() << "Testing" << logRulesPath << "...";
+ foundLoggingRules = QFile::exists(logRulesPath);
+
+ // search the dataPath()
+ // seach app data standard path
+ if(!foundLoggingRules && !isPortable() && dirParam.isEmpty()) {
+ logRulesPath = QStandardPaths::locate(QStandardPaths::AppDataLocation, FS::PathCombine("..", logRulesFile));
+ if(!logRulesPath.isEmpty()) {
+ qDebug() << "Found" << logRulesPath << "...";
+ foundLoggingRules = true;
+ }
+ }
+ // seach root path
+ if(!foundLoggingRules) {
+#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
+ logRulesPath = FS::PathCombine(m_rootPath, "share", BuildConfig.LAUNCHER_NAME, logRulesFile);
+#else
+ logRulesPath = FS::PathCombine(m_rootPath, logRulesFile);
+#endif
+ qDebug() << "Testing" << logRulesPath << "...";
+ foundLoggingRules = QFile::exists(logRulesPath);
+ }
+
+ if(foundLoggingRules) {
+ // load and set logging rules
+ qDebug() << "Loading logging rules from:" << logRulesPath;
+ QSettings loggingRules(logRulesPath, QSettings::IniFormat);
+ loggingRules.beginGroup("Rules");
+ QStringList rule_names = loggingRules.childKeys();
+ QStringList rules;
+ qDebug() << "Setting log rules:";
+ for (auto rule_name : rule_names) {
+ auto rule = QString("%1=%2").arg(rule_name).arg(loggingRules.value(rule_name).toString());
+ rules.append(rule);
+ qDebug() << " " << rule;
+ }
+ auto rules_str = rules.join("\n");
+ QLoggingCategory::setFilterRules(rules_str);
+ }
qDebug() << "<> Log initialized.";
}
@@ -524,6 +572,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
// Language
m_settings->registerSetting("Language", QString());
+ m_settings->registerSetting("UseSystemLocale", false);
// Console
m_settings->registerSetting("ShowConsole", false);
@@ -550,7 +599,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
// Java Settings
m_settings->registerSetting("JavaPath", "");
- m_settings->registerSetting("JavaTimestamp", 0);
+ m_settings->registerSetting("JavaSignature", "");
m_settings->registerSetting("JavaArchitecture", "");
m_settings->registerSetting("JavaRealArchitecture", "");
m_settings->registerSetting("JavaVersion", "");
@@ -643,8 +692,16 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
m_settings->reset("PastebinCustomAPIBase");
}
}
- // meta URL
- m_settings->registerSetting("MetaURLOverride", "");
+ {
+ // Meta URL
+ m_settings->registerSetting("MetaURLOverride", "");
+
+ QUrl metaUrl(m_settings->get("MetaURLOverride").toString());
+
+ // get rid of invalid meta urls
+ if (!metaUrl.isValid() || metaUrl.scheme() != "http" || metaUrl.scheme() != "https")
+ m_settings->reset("MetaURLOverride");
+ }
m_settings->registerSetting("CloseAfterLaunch", false);
m_settings->registerSetting("QuitAfterGameStop", false);
@@ -866,12 +923,7 @@ bool Application::createSetupWizard()
}
return false;
}();
- bool languageRequired = [&]()
- {
- if (settings()->get("Language").toString().isEmpty())
- return true;
- return false;
- }();
+ bool languageRequired = settings()->get("Language").toString().isEmpty();
bool pasteInterventionRequired = settings()->get("PastebinURL") != "";
bool themeInterventionRequired = settings()->get("ApplicationTheme") == "";
bool wizardRequired = javaRequired || languageRequired || pasteInterventionRequired || themeInterventionRequired;
@@ -1515,7 +1567,7 @@ QString Application::getJarPath(QString jarFile)
{
QStringList potentialPaths = {
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
- FS::PathCombine(m_rootPath, "share/" + BuildConfig.LAUNCHER_APP_BINARY_NAME),
+ FS::PathCombine(m_rootPath, "share", BuildConfig.LAUNCHER_NAME),
#endif
FS::PathCombine(m_rootPath, "jars"),
FS::PathCombine(applicationDirPath(), "jars"),
@@ -1655,6 +1707,7 @@ bool Application::handleDataMigration(const QString& currentData,
matcher->add(std::make_shared<SimplePrefixMatcher>(configFile));
matcher->add(std::make_shared<SimplePrefixMatcher>(
BuildConfig.LAUNCHER_CONFIGFILE)); // it's possible that we already used that directory before
+ matcher->add(std::make_shared<SimplePrefixMatcher>("logs/"));
matcher->add(std::make_shared<SimplePrefixMatcher>("accounts.json"));
matcher->add(std::make_shared<SimplePrefixMatcher>("accounts/"));
matcher->add(std::make_shared<SimplePrefixMatcher>("assets/"));
diff --git a/launcher/Application.h b/launcher/Application.h
index 0d24a4e9..ced0af17 100644
--- a/launcher/Application.h
+++ b/launcher/Application.h
@@ -187,6 +187,10 @@ public:
return m_rootPath;
}
+ bool isPortable() {
+ return m_portable;
+ }
+
const Capabilities capabilities() {
return m_capabilities;
}
@@ -275,6 +279,7 @@ private:
QString m_rootPath;
Status m_status = Application::StartingUp;
Capabilities m_capabilities;
+ bool m_portable = false;
#ifdef Q_OS_MACOS
Qt::ApplicationState m_prevAppState = Qt::ApplicationInactive;
diff --git a/launcher/BaseInstance.cpp b/launcher/BaseInstance.cpp
index 8680361c..a8fce879 100644
--- a/launcher/BaseInstance.cpp
+++ b/launcher/BaseInstance.cpp
@@ -40,6 +40,8 @@
#include <QDir>
#include <QDebug>
#include <QRegularExpression>
+#include <QJsonDocument>
+#include <QJsonObject>
#include "settings/INISettingsObject.h"
#include "settings/Setting.h"
@@ -64,6 +66,8 @@ BaseInstance::BaseInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr s
m_settings->registerSetting("totalTimePlayed", 0);
m_settings->registerSetting("lastTimePlayed", 0);
+ m_settings->registerSetting("linkedInstances", "[]");
+
// Game time override
auto gameTimeOverride = m_settings->registerSetting("OverrideGameTime", false);
m_settings->registerOverride(globalSettings->getSetting("ShowGameTime"), gameTimeOverride);
@@ -182,6 +186,38 @@ bool BaseInstance::shouldStopOnConsoleOverflow() const
return m_settings->get("ConsoleOverflowStop").toBool();
}
+QStringList BaseInstance::getLinkedInstances() const
+{
+ return m_settings->get("linkedInstances").toStringList();
+}
+
+void BaseInstance::setLinkedInstances(const QStringList& list)
+{
+ auto linkedInstances = m_settings->get("linkedInstances").toStringList();
+ m_settings->set("linkedInstances", list);
+}
+
+void BaseInstance::addLinkedInstanceId(const QString& id)
+{
+ auto linkedInstances = m_settings->get("linkedInstances").toStringList();
+ linkedInstances.append(id);
+ setLinkedInstances(linkedInstances);
+}
+
+bool BaseInstance::removeLinkedInstanceId(const QString& id)
+{
+ auto linkedInstances = m_settings->get("linkedInstances").toStringList();
+ int numRemoved = linkedInstances.removeAll(id);
+ setLinkedInstances(linkedInstances);
+ return numRemoved > 0;
+}
+
+bool BaseInstance::isLinkedToInstanceId(const QString& id) const
+{
+ auto linkedInstances = m_settings->get("linkedInstances").toStringList();
+ return linkedInstances.contains(id);
+}
+
void BaseInstance::iconUpdated(QString key)
{
if(iconKey() == key)
diff --git a/launcher/BaseInstance.h b/launcher/BaseInstance.h
index a2a4f824..83a8064f 100644
--- a/launcher/BaseInstance.h
+++ b/launcher/BaseInstance.h
@@ -282,6 +282,12 @@ public:
int getConsoleMaxLines() const;
bool shouldStopOnConsoleOverflow() const;
+ QStringList getLinkedInstances() const;
+ void setLinkedInstances(const QStringList& list);
+ void addLinkedInstanceId(const QString& id);
+ bool removeLinkedInstanceId(const QString& id);
+ bool isLinkedToInstanceId(const QString& id) const;
+
protected:
void changeStatus(Status newStatus);
diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt
index 074570e3..7cba97b4 100644
--- a/launcher/CMakeLists.txt
+++ b/launcher/CMakeLists.txt
@@ -26,6 +26,7 @@ set(CORE_SOURCES
MMCZip.cpp
StringUtils.h
StringUtils.cpp
+ QVariantUtils.h
RuntimeContext.h
# Basic instance manipulation tasks (derived from InstanceTask)
@@ -123,6 +124,8 @@ set(NET_SOURCES
net/HttpMetaCache.h
net/MetaCacheSink.cpp
net/MetaCacheSink.h
+ net/Logging.h
+ net/Logging.cpp
net/NetAction.h
net/NetJob.cpp
net/NetJob.h
@@ -359,6 +362,8 @@ set(MINECRAFT_SOURCES
minecraft/mod/tasks/LocalWorldSaveParseTask.cpp
minecraft/mod/tasks/LocalResourceParse.h
minecraft/mod/tasks/LocalResourceParse.cpp
+ minecraft/mod/tasks/GetModDependenciesTask.h
+ minecraft/mod/tasks/GetModDependenciesTask.cpp
# Assets
minecraft/AssetsUtils.h
@@ -372,8 +377,6 @@ set(MINECRAFT_SOURCES
minecraft/services/SkinDelete.cpp
minecraft/services/SkinDelete.h
- mojang/PackageManifest.h
- mojang/PackageManifest.cpp
minecraft/Agent.h)
# the screenshots feature
@@ -484,6 +487,9 @@ set(API_SOURCES
modplatform/helpers/HashUtils.cpp
modplatform/helpers/OverrideUtils.h
modplatform/helpers/OverrideUtils.cpp
+
+ modplatform/helpers/ExportToModList.h
+ modplatform/helpers/ExportToModList.cpp
)
set(FTB_SOURCES
@@ -511,6 +517,8 @@ set(FLAME_SOURCES
modplatform/flame/FlameCheckUpdate.h
modplatform/flame/FlameInstanceCreationTask.h
modplatform/flame/FlameInstanceCreationTask.cpp
+ modplatform/flame/FlamePackExportTask.h
+ modplatform/flame/FlamePackExportTask.cpp
)
set(MODRINTH_SOURCES
@@ -522,13 +530,8 @@ set(MODRINTH_SOURCES
modplatform/modrinth/ModrinthCheckUpdate.h
modplatform/modrinth/ModrinthInstanceCreationTask.cpp
modplatform/modrinth/ModrinthInstanceCreationTask.h
-)
-
-set(MODPACKSCH_SOURCES
- modplatform/modpacksch/FTBPackInstallTask.h
- modplatform/modpacksch/FTBPackInstallTask.cpp
- modplatform/modpacksch/FTBPackManifest.h
- modplatform/modpacksch/FTBPackManifest.cpp
+ modplatform/modrinth/ModrinthPackExportTask.cpp
+ modplatform/modrinth/ModrinthPackExportTask.h
)
set(PACKWIZ_SOURCES
@@ -559,6 +562,18 @@ set(ATLAUNCHER_SOURCES
modplatform/atlauncher/ATLShareCode.h
)
+set(LINKEXE_SOURCES
+ filelink/FileLink.h
+ filelink/FileLink.cpp
+ FileSystem.h
+ FileSystem.cpp
+ Exception.h
+ StringUtils.h
+ StringUtils.cpp
+ DesktopServices.h
+ DesktopServices.cpp
+)
+
######## Logging categories ########
ecm_qt_declare_logging_category(CORE_SOURCES
@@ -570,6 +585,55 @@ ecm_qt_declare_logging_category(CORE_SOURCES
EXPORT "${Launcher_Name}"
)
+ecm_qt_export_logging_category(
+ IDENTIFIER taskLogC
+ CATEGORY_NAME "launcher.task"
+ DEFAULT_SEVERITY Debug
+ DESCRIPTION "Task actions"
+ EXPORT "${Launcher_Name}"
+)
+
+ecm_qt_export_logging_category(
+ IDENTIFIER taskNetLogC
+ CATEGORY_NAME "launcher.task.net"
+ DEFAULT_SEVERITY Debug
+ DESCRIPTION "task network action"
+ EXPORT "${Launcher_Name}"
+)
+
+ecm_qt_export_logging_category(
+ IDENTIFIER taskDownloadLogC
+ CATEGORY_NAME "launcher.task.net.download"
+ DEFAULT_SEVERITY Debug
+ DESCRIPTION "task network download actions"
+ EXPORT "${Launcher_Name}"
+)
+ecm_qt_export_logging_category(
+ IDENTIFIER taskUploadLogC
+ CATEGORY_NAME "launcher.task.net.upload"
+ DEFAULT_SEVERITY Debug
+ DESCRIPTION "task network upload actions"
+ EXPORT "${Launcher_Name}"
+)
+
+ecm_qt_export_logging_category(
+ IDENTIFIER taskMetaCacheLogC
+ CATEGORY_NAME "launcher.task.net.metacache"
+ DEFAULT_SEVERITY Debug
+ DESCRIPTION "task network meta-cache actions"
+ EXPORT "${Launcher_Name}"
+)
+
+ecm_qt_export_logging_category(
+ IDENTIFIER taskHttpMetaCacheLogC
+ CATEGORY_NAME "launcher.task.net.metacache.http"
+ DEFAULT_SEVERITY Debug
+ DESCRIPTION "task network http meta-cache actions"
+ EXPORT "${Launcher_Name}"
+)
+
+
+
if(KDE_INSTALL_LOGGINGCATEGORIESDIR) # only install if there is a standard path for this
ecm_qt_install_logging_categories(
EXPORT "${Launcher_Name}"
@@ -599,7 +663,6 @@ set(LOGIC_SOURCES
${FTB_SOURCES}
${FLAME_SOURCES}
${MODRINTH_SOURCES}
- ${MODPACKSCH_SOURCES}
${PACKWIZ_SOURCES}
${TECHNIC_SOURCES}
${ATLAUNCHER_SOURCES}
@@ -624,6 +687,7 @@ SET(LAUNCHER_SOURCES
VersionProxyModel.h
VersionProxyModel.cpp
Markdown.h
+ Markdown.cpp
# Super secret!
KonamiCode.h
@@ -664,6 +728,10 @@ SET(LAUNCHER_SOURCES
# FIXME: maybe find a better home for this.
SkinUtils.cpp
SkinUtils.h
+ FileIgnoreProxy.cpp
+ FileIgnoreProxy.h
+ FastFileIconProvider.cpp
+ FastFileIconProvider.h
# GUI - setup wizard
ui/setupwizard/SetupWizard.h
@@ -763,8 +831,8 @@ SET(LAUNCHER_SOURCES
ui/pages/global/APIPage.h
# GUI - platform pages
- ui/pages/modplatform/VanillaPage.cpp
- ui/pages/modplatform/VanillaPage.h
+ ui/pages/modplatform/CustomPage.cpp
+ ui/pages/modplatform/CustomPage.h
ui/pages/modplatform/ResourcePage.cpp
ui/pages/modplatform/ResourcePage.h
@@ -797,13 +865,6 @@ SET(LAUNCHER_SOURCES
ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.cpp
ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.h
- ui/pages/modplatform/ftb/FtbFilterModel.cpp
- ui/pages/modplatform/ftb/FtbFilterModel.h
- ui/pages/modplatform/ftb/FtbListModel.cpp
- ui/pages/modplatform/ftb/FtbListModel.h
- ui/pages/modplatform/ftb/FtbPage.cpp
- ui/pages/modplatform/ftb/FtbPage.h
-
ui/pages/modplatform/legacy_ftb/Page.cpp
ui/pages/modplatform/legacy_ftb/Page.h
ui/pages/modplatform/legacy_ftb/ListModel.h
@@ -851,6 +912,10 @@ SET(LAUNCHER_SOURCES
ui/dialogs/EditAccountDialog.h
ui/dialogs/ExportInstanceDialog.cpp
ui/dialogs/ExportInstanceDialog.h
+ ui/dialogs/ExportPackDialog.cpp
+ ui/dialogs/ExportPackDialog.h
+ ui/dialogs/ExportToModListDialog.cpp
+ ui/dialogs/ExportToModListDialog.h
ui/dialogs/IconPickerDialog.cpp
ui/dialogs/IconPickerDialog.h
ui/dialogs/ImportResourceDialog.cpp
@@ -924,6 +989,8 @@ SET(LAUNCHER_SOURCES
ui/widgets/VariableSizedImageObject.cpp
ui/widgets/ProjectItem.h
ui/widgets/ProjectItem.cpp
+ ui/widgets/SubTaskProgressBar.h
+ ui/widgets/SubTaskProgressBar.cpp
ui/widgets/VersionListView.cpp
ui/widgets/VersionListView.h
ui/widgets/VersionSelectWidget.cpp
@@ -973,18 +1040,18 @@ qt_wrap_ui(LAUNCHER_UI
ui/pages/instance/ScreenshotsPage.ui
ui/pages/modplatform/atlauncher/AtlOptionalModDialog.ui
ui/pages/modplatform/atlauncher/AtlPage.ui
- ui/pages/modplatform/VanillaPage.ui
+ ui/pages/modplatform/CustomPage.ui
ui/pages/modplatform/ResourcePage.ui
ui/pages/modplatform/flame/FlamePage.ui
ui/pages/modplatform/legacy_ftb/Page.ui
ui/pages/modplatform/ImportPage.ui
- ui/pages/modplatform/ftb/FtbPage.ui
ui/pages/modplatform/modrinth/ModrinthPage.ui
ui/pages/modplatform/technic/TechnicPage.ui
ui/widgets/InstanceCardWidget.ui
ui/widgets/CustomCommands.ui
ui/widgets/InfoFrame.ui
ui/widgets/ModFilterWidget.ui
+ ui/widgets/SubTaskProgressBar.ui
ui/widgets/ThemeCustomizationWidget.ui
ui/dialogs/CopyInstanceDialog.ui
ui/dialogs/ProfileSetupDialog.ui
@@ -995,6 +1062,8 @@ qt_wrap_ui(LAUNCHER_UI
ui/dialogs/ProfileSelectDialog.ui
ui/dialogs/SkinUploadDialog.ui
ui/dialogs/ExportInstanceDialog.ui
+ ui/dialogs/ExportPackDialog.ui
+ ui/dialogs/ExportToModListDialog.ui
ui/dialogs/IconPickerDialog.ui
ui/dialogs/ImportResourceDialog.ui
ui/dialogs/MSALoginDialog.ui
@@ -1107,6 +1176,41 @@ install(TARGETS ${Launcher_Name}
FRAMEWORK DESTINATION ${FRAMEWORK_DEST_DIR} COMPONENT Runtime
)
+if(WIN32)
+ add_library(filelink_logic STATIC ${LINKEXE_SOURCES})
+ target_include_directories(filelink_logic PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
+ target_link_libraries(filelink_logic
+ systeminfo
+ BuildConfig
+ ghcFilesystem::ghc_filesystem
+ Qt${QT_VERSION_MAJOR}::Widgets
+ Qt${QT_VERSION_MAJOR}::Core
+ Qt${QT_VERSION_MAJOR}::Network
+ # Qt${QT_VERSION_MAJOR}::Concurrent
+ ${Launcher_QT_LIBS}
+ )
+
+ add_executable("${Launcher_Name}_filelink" WIN32 filelink/main.cpp)
+
+ target_sources("${Launcher_Name}_filelink" PRIVATE filelink/filelink.exe.manifest)
+
+ target_link_libraries("${Launcher_Name}_filelink" filelink_logic)
+
+ if(DEFINED Launcher_APP_BINARY_NAME)
+ set_target_properties("${Launcher_Name}_filelink" PROPERTIES OUTPUT_NAME "${Launcher_APP_BINARY_NAME}_filelink")
+ endif()
+ if(DEFINED Launcher_BINARY_RPATH)
+ SET_TARGET_PROPERTIES("${Launcher_Name}_filelink" PROPERTIES INSTALL_RPATH "${Launcher_BINARY_RPATH}")
+ endif()
+
+ install(TARGETS "${Launcher_Name}_filelink"
+ BUNDLE DESTINATION "." COMPONENT Runtime
+ LIBRARY DESTINATION ${LIBRARY_DEST_DIR} COMPONENT Runtime
+ RUNTIME DESTINATION ${BINARY_DEST_DIR} COMPONENT Runtime
+ FRAMEWORK DESTINATION ${FRAMEWORK_DEST_DIR} COMPONENT Runtime
+ )
+endif()
+
if (UNIX AND APPLE)
# Add Sparkle updater
# It has to be copied here instead of just allowing fixup_bundle to install it, otherwise essential parts of
@@ -1123,6 +1227,12 @@ if(INSTALL_BUNDLE STREQUAL "full")
CODE "file(WRITE \"\${CMAKE_INSTALL_PREFIX}/${RESOURCES_DEST_DIR}/qt.conf\" \" \")"
COMPONENT Runtime
)
+ # add qtlogging.ini as a config file
+ install(
+ FILES "qtlogging.ini"
+ DESTINATION ${CMAKE_INSTALL_PREFIX}/${RESOURCES_DEST_DIR}
+ COMPONENT Runtime
+ )
# Bundle plugins
# Image formats
install(
diff --git a/launcher/DesktopServices.cpp b/launcher/DesktopServices.cpp
index 302eaf96..2984a1b4 100644
--- a/launcher/DesktopServices.cpp
+++ b/launcher/DesktopServices.cpp
@@ -37,7 +37,6 @@
#include <QDesktopServices>
#include <QProcess>
#include <QDebug>
-#include "Application.h"
/**
* This shouldn't exist, but until QTBUG-9328 and other unreported bugs are fixed, it needs to be a thing.
diff --git a/launcher/FastFileIconProvider.cpp b/launcher/FastFileIconProvider.cpp
new file mode 100644
index 00000000..f2b6f442
--- /dev/null
+++ b/launcher/FastFileIconProvider.cpp
@@ -0,0 +1,47 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2023 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 "FastFileIconProvider.h"
+
+#include <QApplication>
+#include <QStyle>
+
+QIcon FastFileIconProvider::icon(const QFileInfo& info) const
+{
+#if QT_VERSION >= QT_VERSION_CHECK(6, 4, 0)
+ bool link = info.isSymbolicLink() || info.isAlias() || info.isShortcut();
+#else
+ // in versions prior to 6.4 we don't have access to isAlias
+ bool link = info.isSymLink();
+#endif
+ QStyle::StandardPixmap icon;
+
+ if (info.isDir()) {
+ if (link)
+ icon = QStyle::SP_DirLinkIcon;
+ else
+ icon = QStyle::SP_DirIcon;
+ } else {
+ if (link)
+ icon = QStyle::SP_FileLinkIcon;
+ else
+ icon = QStyle::SP_FileIcon;
+ }
+
+ return QApplication::style()->standardIcon(icon);
+} \ No newline at end of file
diff --git a/launcher/FastFileIconProvider.h b/launcher/FastFileIconProvider.h
new file mode 100644
index 00000000..20853404
--- /dev/null
+++ b/launcher/FastFileIconProvider.h
@@ -0,0 +1,26 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2023 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 <QFileIconProvider>
+
+class FastFileIconProvider : public QFileIconProvider {
+ public:
+ QIcon icon(const QFileInfo& info) const override;
+}; \ No newline at end of file
diff --git a/launcher/FileIgnoreProxy.cpp b/launcher/FileIgnoreProxy.cpp
new file mode 100644
index 00000000..4c8c64c7
--- /dev/null
+++ b/launcher/FileIgnoreProxy.cpp
@@ -0,0 +1,279 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ * Copyright (C) 2023 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 "FileIgnoreProxy.h"
+
+#include <QDebug>
+#include <QFileSystemModel>
+#include <QSortFilterProxyModel>
+#include <QStack>
+#include <algorithm>
+#include "FileSystem.h"
+#include "SeparatorPrefixTree.h"
+#include "StringUtils.h"
+
+FileIgnoreProxy::FileIgnoreProxy(QString root, QObject* parent) : QSortFilterProxyModel(parent), root(root) {}
+// NOTE: Sadly, we have to do sorting ourselves.
+bool FileIgnoreProxy::lessThan(const QModelIndex& left, const QModelIndex& right) const
+{
+ QFileSystemModel* fsm = qobject_cast<QFileSystemModel*>(sourceModel());
+ if (!fsm) {
+ return QSortFilterProxyModel::lessThan(left, right);
+ }
+ bool asc = sortOrder() == Qt::AscendingOrder ? true : false;
+
+ QFileInfo leftFileInfo = fsm->fileInfo(left);
+ QFileInfo rightFileInfo = fsm->fileInfo(right);
+
+ if (!leftFileInfo.isDir() && rightFileInfo.isDir()) {
+ return !asc;
+ }
+ if (leftFileInfo.isDir() && !rightFileInfo.isDir()) {
+ return asc;
+ }
+
+ // sort and proxy model breaks the original model...
+ if (sortColumn() == 0) {
+ return StringUtils::naturalCompare(leftFileInfo.fileName(), rightFileInfo.fileName(), Qt::CaseInsensitive) < 0;
+ }
+ if (sortColumn() == 1) {
+ auto leftSize = leftFileInfo.size();
+ auto rightSize = rightFileInfo.size();
+ if ((leftSize == rightSize) || (leftFileInfo.isDir() && rightFileInfo.isDir())) {
+ return StringUtils::naturalCompare(leftFileInfo.fileName(), rightFileInfo.fileName(), Qt::CaseInsensitive) < 0 ? asc : !asc;
+ }
+ return leftSize < rightSize;
+ }
+ return QSortFilterProxyModel::lessThan(left, right);
+}
+
+Qt::ItemFlags FileIgnoreProxy::flags(const QModelIndex& index) const
+{
+ if (!index.isValid())
+ return Qt::NoItemFlags;
+
+ auto sourceIndex = mapToSource(index);
+ Qt::ItemFlags flags = sourceIndex.flags();
+ if (index.column() == 0) {
+ flags |= Qt::ItemIsUserCheckable;
+ if (sourceIndex.model()->hasChildren(sourceIndex)) {
+ flags |= Qt::ItemIsAutoTristate;
+ }
+ }
+
+ return flags;
+}
+
+QVariant FileIgnoreProxy::data(const QModelIndex& index, int role) const
+{
+ QModelIndex sourceIndex = mapToSource(index);
+
+ if (index.column() == 0 && role == Qt::CheckStateRole) {
+ QFileSystemModel* fsm = qobject_cast<QFileSystemModel*>(sourceModel());
+ auto blockedPath = relPath(fsm->filePath(sourceIndex));
+ auto cover = blocked.cover(blockedPath);
+ if (!cover.isNull()) {
+ return QVariant(Qt::Unchecked);
+ } else if (blocked.exists(blockedPath)) {
+ return QVariant(Qt::PartiallyChecked);
+ } else {
+ return QVariant(Qt::Checked);
+ }
+ }
+
+ return sourceIndex.data(role);
+}
+
+bool FileIgnoreProxy::setData(const QModelIndex& index, const QVariant& value, int role)
+{
+ if (index.column() == 0 && role == Qt::CheckStateRole) {
+ Qt::CheckState state = static_cast<Qt::CheckState>(value.toInt());
+ return setFilterState(index, state);
+ }
+
+ QModelIndex sourceIndex = mapToSource(index);
+ return QSortFilterProxyModel::sourceModel()->setData(sourceIndex, value, role);
+}
+
+QString FileIgnoreProxy::relPath(const QString& path) const
+{
+ return QDir(root).relativeFilePath(path);
+}
+
+bool FileIgnoreProxy::setFilterState(QModelIndex index, Qt::CheckState state)
+{
+ QFileSystemModel* fsm = qobject_cast<QFileSystemModel*>(sourceModel());
+
+ if (!fsm) {
+ return false;
+ }
+
+ QModelIndex sourceIndex = mapToSource(index);
+ auto blockedPath = relPath(fsm->filePath(sourceIndex));
+ bool changed = false;
+ if (state == Qt::Unchecked) {
+ // blocking a path
+ auto& node = blocked.insert(blockedPath);
+ // get rid of all blocked nodes below
+ node.clear();
+ changed = true;
+ } else if (state == Qt::Checked || state == Qt::PartiallyChecked) {
+ if (!blocked.remove(blockedPath)) {
+ auto cover = blocked.cover(blockedPath);
+ qDebug() << "Blocked by cover" << cover;
+ // uncover
+ blocked.remove(cover);
+ // block all contents, except for any cover
+ QModelIndex rootIndex = fsm->index(FS::PathCombine(root, cover));
+ QModelIndex doing = rootIndex;
+ int row = 0;
+ QStack<QModelIndex> todo;
+ while (1) {
+ auto node = fsm->index(row, 0, doing);
+ if (!node.isValid()) {
+ if (!todo.size()) {
+ break;
+ } else {
+ doing = todo.pop();
+ row = 0;
+ continue;
+ }
+ }
+ auto relpath = relPath(fsm->filePath(node));
+ if (blockedPath.startsWith(relpath)) // cover found?
+ {
+ // continue processing cover later
+ todo.push(node);
+ } else {
+ // or just block this one.
+ blocked.insert(relpath);
+ }
+ row++;
+ }
+ }
+ changed = true;
+ }
+ if (changed) {
+ // update the thing
+ emit dataChanged(index, index, { Qt::CheckStateRole });
+ // update everything above index
+ QModelIndex up = index.parent();
+ while (1) {
+ if (!up.isValid())
+ break;
+ emit dataChanged(up, up, { Qt::CheckStateRole });
+ up = up.parent();
+ }
+ // and everything below the index
+ QModelIndex doing = index;
+ int row = 0;
+ QStack<QModelIndex> todo;
+ while (1) {
+ auto node = this->index(row, 0, doing);
+ if (!node.isValid()) {
+ if (!todo.size()) {
+ break;
+ } else {
+ doing = todo.pop();
+ row = 0;
+ continue;
+ }
+ }
+ emit dataChanged(node, node, { Qt::CheckStateRole });
+ todo.push(node);
+ row++;
+ }
+ // siblings and unrelated nodes are ignored
+ }
+ return true;
+}
+
+bool FileIgnoreProxy::shouldExpand(QModelIndex index)
+{
+ QModelIndex sourceIndex = mapToSource(index);
+ QFileSystemModel* fsm = qobject_cast<QFileSystemModel*>(sourceModel());
+ if (!fsm) {
+ return false;
+ }
+ auto blockedPath = relPath(fsm->filePath(sourceIndex));
+ auto found = blocked.find(blockedPath);
+ if (found) {
+ return !found->leaf();
+ }
+ return false;
+}
+
+void FileIgnoreProxy::setBlockedPaths(QStringList paths)
+{
+ beginResetModel();
+ blocked.clear();
+ blocked.insert(paths);
+ endResetModel();
+}
+
+bool FileIgnoreProxy::filterAcceptsColumn(int source_column, const QModelIndex& source_parent) const
+{
+ Q_UNUSED(source_parent)
+
+ // adjust the columns you want to filter out here
+ // return false for those that will be hidden
+ if (source_column == 2 || source_column == 3)
+ return false;
+
+ return true;
+}
+
+bool FileIgnoreProxy::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const
+{
+ QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
+ QFileSystemModel* fsm = qobject_cast<QFileSystemModel*>(sourceModel());
+
+ auto fileInfo = fsm->fileInfo(index);
+ return !ignoreFile(fileInfo);
+}
+
+bool FileIgnoreProxy::ignoreFile(QFileInfo fileInfo) const
+{
+ auto fileName = fileInfo.fileName();
+ auto path = relPath(fileInfo.absoluteFilePath());
+ return std::any_of(m_ignoreFiles.cbegin(), m_ignoreFiles.cend(), [fileName](auto iFileName) { return fileName == iFileName; }) ||
+ m_ignoreFilePaths.covers(path);
+}
+
+bool FileIgnoreProxy::filterFile(const QString& fileName) const
+{
+ return blocked.covers(fileName) || ignoreFile(QFileInfo(QDir(root), fileName));
+}
diff --git a/launcher/FileIgnoreProxy.h b/launcher/FileIgnoreProxy.h
new file mode 100644
index 00000000..e01a2651
--- /dev/null
+++ b/launcher/FileIgnoreProxy.h
@@ -0,0 +1,85 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ * Copyright (C) 2023 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.
+ */
+
+#pragma once
+
+#include <QFileInfo>
+#include <QSortFilterProxyModel>
+#include "SeparatorPrefixTree.h"
+
+class FileIgnoreProxy : public QSortFilterProxyModel {
+ Q_OBJECT
+
+ public:
+ FileIgnoreProxy(QString root, QObject* parent);
+ // NOTE: Sadly, we have to do sorting ourselves.
+ bool lessThan(const QModelIndex& left, const QModelIndex& right) const;
+
+ virtual Qt::ItemFlags flags(const QModelIndex& index) const;
+
+ virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const;
+ virtual bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole);
+
+ QString relPath(const QString& path) const;
+
+ bool setFilterState(QModelIndex index, Qt::CheckState state);
+
+ bool shouldExpand(QModelIndex index);
+
+ void setBlockedPaths(QStringList paths);
+
+ inline const SeparatorPrefixTree<'/'>& blockedPaths() const { return blocked; }
+ inline SeparatorPrefixTree<'/'>& blockedPaths() { return blocked; }
+
+ // list of file names that need to be removed completely from model
+ inline QStringList& ignoreFilesWithName() { return m_ignoreFiles; }
+ // list of relative paths that need to be removed completely from model
+ inline SeparatorPrefixTree<'/'>& ignoreFilesWithPath() { return m_ignoreFilePaths; }
+
+ bool filterFile(const QString& fileName) const;
+
+ protected:
+ bool filterAcceptsColumn(int source_column, const QModelIndex& source_parent) const;
+ bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const;
+
+ bool ignoreFile(QFileInfo file) const;
+
+ private:
+ const QString root;
+ SeparatorPrefixTree<'/'> blocked;
+ QStringList m_ignoreFiles;
+ SeparatorPrefixTree<'/'> m_ignoreFilePaths;
+};
diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp
index aee5245d..4538702f 100644
--- a/launcher/FileSystem.cpp
+++ b/launcher/FileSystem.cpp
@@ -3,6 +3,7 @@
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
+ * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.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
@@ -35,6 +36,9 @@
*/
#include "FileSystem.h"
+#include <QPair>
+
+#include "BuildConfig.h"
#include <QDebug>
#include <QDir>
@@ -43,13 +47,17 @@
#include <QFileInfo>
#include <QSaveFile>
#include <QStandardPaths>
+#include <QStorageInfo>
#include <QTextStream>
#include <QUrl>
+#include <QtNetwork>
+#include <system_error>
#include "DesktopServices.h"
#include "StringUtils.h"
#if defined Q_OS_WIN32
+#define NOMINMAX
#define WIN32_LEAN_AND_MEAN
#include <objbase.h>
#include <objidl.h>
@@ -61,6 +69,10 @@
#include <windows.h>
#include <winnls.h>
#include <string>
+// for ShellExecute
+#include <Shellapi.h>
+#include <objbase.h>
+#include <shlobj.h>
#else
#include <utime.h>
#endif
@@ -68,22 +80,96 @@
// Snippet from https://github.com/gulrak/filesystem#using-it-as-single-file-header
#ifdef __APPLE__
-#include <Availability.h> // for deployment target to support pre-catalina targets without std::fs
-#endif // __APPLE__
+#include <Availability.h> // for deployment target to support pre-catalina targets without std::fs
+#endif // __APPLE__
#if ((defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) || (defined(__cplusplus) && __cplusplus >= 201703L)) && defined(__has_include)
#if __has_include(<filesystem>) && (!defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || __MAC_OS_X_VERSION_MIN_REQUIRED >= 101500)
#define GHC_USE_STD_FS
#include <filesystem>
namespace fs = std::filesystem;
-#endif // MacOS min version check
-#endif // Other OSes version check
+#endif // MacOS min version check
+#endif // Other OSes version check
#ifndef GHC_USE_STD_FS
#include <ghc/filesystem.hpp>
namespace fs = ghc::filesystem;
#endif
+// clone
+#if defined(Q_OS_LINUX)
+#include <errno.h>
+#include <fcntl.h> /* Definition of FICLONE* constants */
+#include <linux/fs.h>
+#include <sys/ioctl.h>
+#include <unistd.h>
+#elif defined(Q_OS_MACOS)
+#include <sys/attr.h>
+#include <sys/clonefile.h>
+#elif defined(Q_OS_WIN)
+// winbtrfs clone vs rundll32 shellbtrfs.dll,ReflinkCopy
+#include <fileapi.h>
+#include <stdio.h>
+#include <tchar.h>
+#include <windows.h>
+// refs
+#include <winioctl.h>
+#if defined(__MINGW32__)
+#include <crtdbg.h>
+#endif
+#endif
+
+#if defined(Q_OS_WIN)
+
+#if defined(__MINGW32__)
+
+typedef struct _DUPLICATE_EXTENTS_DATA {
+ HANDLE FileHandle;
+ LARGE_INTEGER SourceFileOffset;
+ LARGE_INTEGER TargetFileOffset;
+ LARGE_INTEGER ByteCount;
+} DUPLICATE_EXTENTS_DATA, *PDUPLICATE_EXTENTS_DATA;
+
+typedef struct _FSCTL_GET_INTEGRITY_INFORMATION_BUFFER {
+ WORD ChecksumAlgorithm; // Checksum algorithm. e.g. CHECKSUM_TYPE_UNCHANGED, CHECKSUM_TYPE_NONE, CHECKSUM_TYPE_CRC32
+ WORD Reserved; // Must be 0
+ DWORD Flags; // FSCTL_INTEGRITY_FLAG_xxx
+ DWORD ChecksumChunkSizeInBytes;
+ DWORD ClusterSizeInBytes;
+} FSCTL_GET_INTEGRITY_INFORMATION_BUFFER, *PFSCTL_GET_INTEGRITY_INFORMATION_BUFFER;
+
+typedef struct _FSCTL_SET_INTEGRITY_INFORMATION_BUFFER {
+ WORD ChecksumAlgorithm; // Checksum algorithm. e.g. CHECKSUM_TYPE_UNCHANGED, CHECKSUM_TYPE_NONE, CHECKSUM_TYPE_CRC32
+ WORD Reserved; // Must be 0
+ DWORD Flags; // FSCTL_INTEGRITY_FLAG_xxx
+} FSCTL_SET_INTEGRITY_INFORMATION_BUFFER, *PFSCTL_SET_INTEGRITY_INFORMATION_BUFFER;
+
+#endif
+
+#ifndef FSCTL_DUPLICATE_EXTENTS_TO_FILE
+#define FSCTL_DUPLICATE_EXTENTS_TO_FILE CTL_CODE(FILE_DEVICE_FILE_SYSTEM, 209, METHOD_BUFFERED, FILE_WRITE_DATA)
+#endif
+
+#ifndef FSCTL_GET_INTEGRITY_INFORMATION
+#define FSCTL_GET_INTEGRITY_INFORMATION \
+ CTL_CODE(FILE_DEVICE_FILE_SYSTEM, 159, METHOD_BUFFERED, FILE_ANY_ACCESS) // FSCTL_GET_INTEGRITY_INFORMATION_BUFFER
+#endif
+
+#ifndef FSCTL_SET_INTEGRITY_INFORMATION
+#define FSCTL_SET_INTEGRITY_INFORMATION \
+ CTL_CODE(FILE_DEVICE_FILE_SYSTEM, 160, METHOD_BUFFERED, FILE_READ_DATA | FILE_WRITE_DATA) // FSCTL_SET_INTEGRITY_INFORMATION_BUFFER
+#endif
+
+#ifndef ERROR_NOT_CAPABLE
+#define ERROR_NOT_CAPABLE 775L
+#endif
+
+#ifndef ERROR_BLOCK_TOO_MANY_REFERENCES
+#define ERROR_BLOCK_TOO_MANY_REFERENCES 347L
+#endif
+
+#endif
+
namespace FS {
void ensureExists(const QDir& dir)
@@ -152,13 +238,16 @@ bool ensureFolderPathExists(QString foldernamepath)
return success;
}
-/// @brief Copies a directory and it's contents from src to dest
-/// @param offset subdirectory form src to copy to dest
-/// @return if there was an error during the filecopy
+/**
+ * @brief Copies a directory and it's contents from src to dest
+ * @param offset subdirectory form src to copy to dest
+ * @return if there was an error during the filecopy
+ */
bool copy::operator()(const QString& offset, bool dryRun)
{
using copy_opts = fs::copy_options;
m_copied = 0; // reset counter
+ m_failedPaths.clear();
// NOTE always deep copy on windows. the alternatives are too messy.
#if defined Q_OS_WIN32
@@ -190,6 +279,9 @@ bool copy::operator()(const QString& offset, bool dryRun)
qWarning() << "Failed to copy files:" << QString::fromStdString(err.message());
qDebug() << "Source file:" << src_path;
qDebug() << "Destination file:" << dst_path;
+ m_failedPaths.append(dst_path);
+ emit copyFailed(relative_dst_path);
+ return;
}
m_copied++;
emit fileCopied(relative_dst_path);
@@ -215,6 +307,271 @@ bool copy::operator()(const QString& offset, bool dryRun)
return err.value() == 0;
}
+/// qDebug print support for the LinkPair struct
+QDebug operator<<(QDebug debug, const LinkPair& lp)
+{
+ QDebugStateSaver saver(debug);
+
+ debug.nospace() << "LinkPair{ src: " << lp.src << " , dst: " << lp.dst << " }";
+ return debug;
+}
+
+bool create_link::operator()(const QString& offset, bool dryRun)
+{
+ m_linked = 0; // reset counter
+ m_path_results.clear();
+ m_links_to_make.clear();
+
+ m_path_results.clear();
+
+ make_link_list(offset);
+
+ if (!dryRun)
+ return make_links();
+
+ return true;
+}
+
+/**
+ * @brief Make a list of all the links to make
+ * @param offset subdirectory of src to link to dest
+ */
+void create_link::make_link_list(const QString& offset)
+{
+ for (auto pair : m_path_pairs) {
+ const QString& srcPath = pair.src;
+ const QString& dstPath = pair.dst;
+
+ auto src = PathCombine(QDir(srcPath).absolutePath(), offset);
+ auto dst = PathCombine(QDir(dstPath).absolutePath(), offset);
+
+ // you can't hard link a directory so make sure if we deal with a directory we do so recursively
+ if (m_useHardLinks)
+ m_recursive = true;
+
+ // Function that'll do the actual linking
+ auto link_file = [&](QString src_path, QString relative_dst_path) {
+ if (m_matcher && (m_matcher->matches(relative_dst_path) != m_whitelist)) {
+ qDebug() << "path" << relative_dst_path << "in black list or not in whitelist";
+ return;
+ }
+
+ auto dst_path = PathCombine(dst, relative_dst_path);
+ LinkPair link = { src_path, dst_path };
+ m_links_to_make.append(link);
+ };
+
+ if ((!m_recursive) || !fs::is_directory(StringUtils::toStdString(src))) {
+ if (m_debug)
+ qDebug() << "linking single file or dir:" << src << "to" << dst;
+ link_file(src, "");
+ } else {
+ if (m_debug)
+ qDebug() << "linking recursively:" << src << "to" << dst << ", max_depth:" << m_max_depth;
+ QDir src_dir(src);
+ QDirIterator source_it(src, QDir::Filter::Files | QDir::Filter::Hidden, QDirIterator::Subdirectories);
+
+ QStringList linkedPaths;
+
+ while (source_it.hasNext()) {
+ auto src_path = source_it.next();
+ auto relative_path = src_dir.relativeFilePath(src_path);
+
+ if (m_max_depth >= 0 && pathDepth(relative_path) > m_max_depth) {
+ relative_path = pathTruncate(relative_path, m_max_depth);
+ src_path = src_dir.filePath(relative_path);
+ if (linkedPaths.contains(src_path)) {
+ continue;
+ }
+ }
+
+ linkedPaths.append(src_path);
+
+ link_file(src_path, relative_path);
+ }
+ }
+ }
+}
+
+bool create_link::make_links()
+{
+ for (auto link : m_links_to_make) {
+ QString src_path = link.src;
+ QString dst_path = link.dst;
+ auto src_path_std = StringUtils::toStdString(link.src);
+ auto dst_path_std = StringUtils::toStdString(link.dst);
+
+ ensureFilePathExists(dst_path);
+ if (m_useHardLinks) {
+ if (m_debug)
+ qDebug() << "making hard link:" << src_path << "to" << dst_path;
+ fs::create_hard_link(src_path_std, dst_path_std, m_os_err);
+ } else if (fs::is_directory(src_path_std)) {
+ if (m_debug)
+ qDebug() << "making directory_symlink:" << src_path << "to" << dst_path;
+ fs::create_directory_symlink(src_path_std, dst_path_std, m_os_err);
+ } else {
+ if (m_debug)
+ qDebug() << "making symlink:" << src_path << "to" << dst_path;
+ fs::create_symlink(src_path_std, dst_path_std, m_os_err);
+ }
+
+ if (m_os_err) {
+ qWarning() << "Failed to link files:" << QString::fromStdString(m_os_err.message());
+ qDebug() << "Source file:" << src_path;
+ qDebug() << "Destination file:" << dst_path;
+ qDebug() << "Error category:" << m_os_err.category().name();
+ qDebug() << "Error code:" << m_os_err.value();
+ emit linkFailed(src_path, dst_path, QString::fromStdString(m_os_err.message()), m_os_err.value());
+ } else {
+ m_linked++;
+ emit fileLinked(src_path, dst_path);
+ }
+ if (m_os_err)
+ return false;
+ }
+ return true;
+}
+
+void create_link::runPrivileged(const QString& offset)
+{
+ m_linked = 0; // reset counter
+ m_path_results.clear();
+ m_links_to_make.clear();
+
+ bool gotResults = false;
+
+ make_link_list(offset);
+
+ QString serverName = BuildConfig.LAUNCHER_APP_BINARY_NAME + "_filelink_server" + StringUtils::getRandomAlphaNumeric();
+
+ connect(&m_linkServer, &QLocalServer::newConnection, this, [&]() {
+ qDebug() << "Client connected, sending out pairs";
+ // construct block of data to send
+ QByteArray block;
+ QDataStream out(&block, QIODevice::WriteOnly);
+
+ qint32 blocksize = quint32(sizeof(quint32));
+ for (auto link : m_links_to_make) {
+ blocksize += quint32(link.src.size());
+ blocksize += quint32(link.dst.size());
+ }
+ qDebug() << "About to write block of size:" << blocksize;
+ out << blocksize;
+
+ out << quint32(m_links_to_make.length());
+ for (auto link : m_links_to_make) {
+ out << link.src;
+ out << link.dst;
+ }
+
+ QLocalSocket* clientConnection = m_linkServer.nextPendingConnection();
+ connect(clientConnection, &QLocalSocket::disconnected, clientConnection, &QLocalSocket::deleteLater);
+
+ connect(clientConnection, &QLocalSocket::readyRead, this, [&, clientConnection]() {
+ QDataStream in;
+ quint32 blockSize = 0;
+ in.setDevice(clientConnection);
+
+ qDebug() << "Reading path results from client";
+ qDebug() << "bytes available" << clientConnection->bytesAvailable();
+
+ // Relies on the fact that QDataStream serializes a quint32 into
+ // sizeof(quint32) bytes
+ if (clientConnection->bytesAvailable() < (int)sizeof(quint32))
+ return;
+ qDebug() << "reading block size";
+ in >> blockSize;
+
+ qDebug() << "blocksize is" << blockSize;
+ qDebug() << "bytes available" << clientConnection->bytesAvailable();
+ if (clientConnection->bytesAvailable() < blockSize || in.atEnd())
+ return;
+
+ quint32 numResults;
+ in >> numResults;
+ qDebug() << "numResults" << numResults;
+
+ for (quint32 i = 0; i < numResults; i++) {
+ FS::LinkResult result;
+ in >> result.src;
+ in >> result.dst;
+ in >> result.err_msg;
+ qint32 err_value;
+ in >> err_value;
+ result.err_value = err_value;
+ if (result.err_value) {
+ qDebug() << "privileged link fail" << result.src << "to" << result.dst << "code" << result.err_value << result.err_msg;
+ emit linkFailed(result.src, result.dst, result.err_msg, result.err_value);
+ } else {
+ qDebug() << "privileged link success" << result.src << "to" << result.dst;
+ m_linked++;
+ emit fileLinked(result.src, result.dst);
+ }
+ m_path_results.append(result);
+ }
+ gotResults = true;
+ qDebug() << "results received, closing connection";
+ clientConnection->close();
+ });
+
+ qint64 byteswritten = clientConnection->write(block);
+ bool bytesflushed = clientConnection->flush();
+ qDebug() << "block flushed" << byteswritten << bytesflushed;
+ });
+
+ qDebug() << "Listening on pipe" << serverName;
+ if (!m_linkServer.listen(serverName)) {
+ qDebug() << "Unable to start local pipe server on" << serverName << ":" << m_linkServer.errorString();
+ return;
+ }
+
+ ExternalLinkFileProcess* linkFileProcess = new ExternalLinkFileProcess(serverName, m_useHardLinks, this);
+ connect(linkFileProcess, &ExternalLinkFileProcess::processExited, this, [&]() { emit finishedPrivileged(gotResults); });
+ connect(linkFileProcess, &ExternalLinkFileProcess::finished, linkFileProcess, &QObject::deleteLater);
+
+ linkFileProcess->start();
+}
+
+void ExternalLinkFileProcess::runLinkFile()
+{
+ QString fileLinkExe =
+ PathCombine(QCoreApplication::instance()->applicationDirPath(), BuildConfig.LAUNCHER_APP_BINARY_NAME + "_filelink");
+ QString params = "-s " + m_server;
+
+ params += " -H " + QVariant(m_useHardLinks).toString();
+
+#if defined Q_OS_WIN32
+ SHELLEXECUTEINFO ShExecInfo;
+
+ fileLinkExe = fileLinkExe + ".exe";
+
+ qDebug() << "Running: runas" << fileLinkExe << params;
+
+ LPCWSTR programNameWin = (const wchar_t*)fileLinkExe.utf16();
+ LPCWSTR paramsWin = (const wchar_t*)params.utf16();
+
+ // https://learn.microsoft.com/en-us/windows/win32/api/shellapi/ns-shellapi-shellexecuteinfoa
+ ShExecInfo.cbSize = sizeof(SHELLEXECUTEINFO);
+ ShExecInfo.fMask = SEE_MASK_NOCLOSEPROCESS;
+ ShExecInfo.hwnd = NULL; // Optional. A handle to the owner window, used to display and position any UI that the system might produce
+ // while executing this function.
+ ShExecInfo.lpVerb = L"runas"; // elevate to admin, show UAC
+ ShExecInfo.lpFile = programNameWin;
+ ShExecInfo.lpParameters = paramsWin;
+ ShExecInfo.lpDirectory = NULL;
+ ShExecInfo.nShow = SW_HIDE;
+ ShExecInfo.hInstApp = NULL;
+
+ ShellExecuteEx(&ShExecInfo);
+
+ WaitForSingleObject(ShExecInfo.hProcess, INFINITE);
+ CloseHandle(ShExecInfo.hProcess);
+#endif
+
+ qDebug() << "Process exited";
+}
+
bool move(const QString& source, const QString& dest)
{
std::error_code err;
@@ -244,7 +601,7 @@ bool deletePath(QString path)
return err.value() == 0;
}
-bool trash(QString path, QString *pathInTrash)
+bool trash(QString path, QString* pathInTrash)
{
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
return false;
@@ -279,11 +636,60 @@ QString PathCombine(const QString& path1, const QString& path2, const QString& p
return PathCombine(PathCombine(path1, path2, path3), path4);
}
-QString AbsolutePath(QString path)
+QString AbsolutePath(const QString& path)
{
return QFileInfo(path).absolutePath();
}
+int pathDepth(const QString& path)
+{
+ if (path.isEmpty())
+ return 0;
+
+ QFileInfo info(path);
+
+#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
+ auto parts = QDir::toNativeSeparators(info.path()).split(QDir::separator(), QString::SkipEmptyParts);
+#else
+ auto parts = QDir::toNativeSeparators(info.path()).split(QDir::separator(), Qt::SkipEmptyParts);
+#endif
+
+ int numParts = parts.length();
+ numParts -= parts.count(".");
+ numParts -= parts.count("..") * 2;
+
+ return numParts;
+}
+
+QString pathTruncate(const QString& path, int depth)
+{
+ if (path.isEmpty() || (depth < 0))
+ return "";
+
+ QString trunc = QFileInfo(path).path();
+
+ if (pathDepth(trunc) > depth) {
+ return pathTruncate(trunc, depth);
+ }
+
+#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
+ auto parts = QDir::toNativeSeparators(trunc).split(QDir::separator(), QString::SkipEmptyParts);
+#else
+ auto parts = QDir::toNativeSeparators(trunc).split(QDir::separator(), Qt::SkipEmptyParts);
+#endif
+
+ if (parts.startsWith(".") && !path.startsWith(".")) {
+ parts.removeFirst();
+ }
+ if (QDir::toNativeSeparators(path).startsWith(QDir::separator())) {
+ parts.prepend("");
+ }
+
+ trunc = parts.join(QDir::separator());
+
+ return trunc;
+}
+
QString ResolveExecutable(QString path)
{
if (path.isEmpty()) {
@@ -368,10 +774,47 @@ QString getDesktopDir()
// Cross-platform Shortcut creation
bool createShortcut(QString destination, QString target, QStringList args, QString name, QString icon)
{
+ if (destination.isEmpty()) {
+ destination = PathCombine(getDesktopDir(), RemoveInvalidFilenameChars(name));
+ }
#if defined(Q_OS_MACOS)
- destination += ".command";
+ // Create the Application
+ QDir applicationDirectory = QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation) + "/" + BuildConfig.LAUNCHER_NAME + " Instances/";
- QFile f(destination);
+ if (!applicationDirectory.mkpath(".")) {
+ qWarning() << "Couldn't create application directory";
+ return false;
+ }
+
+ QDir application = applicationDirectory.path() + "/" + name + ".app/";
+
+ if (application.exists()) {
+ qWarning() << "Application already exists!";
+ return false;
+ }
+
+ if (!application.mkpath(".")) {
+ qWarning() << "Couldn't create application";
+ return false;
+ }
+
+ QDir content = application.path() + "/Contents/";
+ QDir resources = content.path() + "/Resources/";
+ QDir binaryDir = content.path() + "/MacOS/";
+ QFile info = content.path() + "/Info.plist";
+
+ if (!(content.mkpath(".") && resources.mkpath(".") && binaryDir.mkpath("."))) {
+ qWarning() << "Couldn't create directories within application";
+ return false;
+ }
+ info.open(QIODevice::WriteOnly | QIODevice::Text);
+
+ QFile(icon).rename(resources.path() + "/Icon.icns");
+
+ // Create the Command file
+ QString exec = binaryDir.path() + "/Run.command";
+
+ QFile f(exec);
f.open(QIODevice::WriteOnly | QIODevice::Text);
QTextStream stream(&f);
@@ -381,19 +824,39 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri
stream << "#!/bin/bash"
<< "\n";
- stream << "\""
- << target
- << "\" "
- << argstring
- << "\n";
+ stream << "\"" << target << "\" " << argstring << "\n";
stream.flush();
f.close();
f.setPermissions(f.permissions() | QFileDevice::ExeOwner | QFileDevice::ExeGroup | QFileDevice::ExeOther);
+ // Generate the Info.plist
+ QTextStream infoStream(&info);
+ infoStream << "<?xml version=\"1.0\" encoding=\"UTF-8\"?> \n"
+ "<!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\" "
+ "\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">"
+ "<plist version=\"1.0\">\n"
+ "<dict>\n"
+ " <key>CFBundleExecutable</key>\n"
+ " <string>Run.command</string>\n" // The path to the executable
+ " <key>CFBundleIconFile</key>\n"
+ " <string>Icon.icns</string>\n"
+ " <key>CFBundleName</key>\n"
+ " <string>" << name << "</string>\n" // Name of the application
+ " <key>CFBundlePackageType</key>\n"
+ " <string>APPL</string>\n"
+ " <key>CFBundleShortVersionString</key>\n"
+ " <string>1.0</string>\n"
+ " <key>CFBundleVersion</key>\n"
+ " <string>1.0</string>\n"
+ "</dict>\n"
+ "</plist>";
+
return true;
#elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
+ if (!destination.endsWith(".desktop")) // in case of isFlatpak destination is already populated
+ destination += ".desktop";
QFile f(destination);
f.open(QIODevice::WriteOnly | QIODevice::Text);
QTextStream stream(&f);
@@ -408,8 +871,7 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri
<< "\n";
stream << "Exec=\"" << target.toLocal8Bit() << "\"" << argstring.toLocal8Bit() << "\n";
stream << "Name=" << name.toLocal8Bit() << "\n";
- if (!icon.isEmpty())
- {
+ if (!icon.isEmpty()) {
stream << "Icon=" << icon.toLocal8Bit() << "\n";
}
@@ -422,55 +884,45 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri
#elif defined(Q_OS_WIN)
QFileInfo targetInfo(target);
- if (!targetInfo.exists())
- {
+ if (!targetInfo.exists()) {
qWarning() << "Target file does not exist!";
return false;
}
target = targetInfo.absoluteFilePath();
- if (target.length() >= MAX_PATH)
- {
+ if (target.length() >= MAX_PATH) {
qWarning() << "Target file path is too long!";
return false;
}
- if (!icon.isEmpty() && icon.length() >= MAX_PATH)
- {
+ if (!icon.isEmpty() && icon.length() >= MAX_PATH) {
qWarning() << "Icon path is too long!";
return false;
}
destination += ".lnk";
- if (destination.length() >= MAX_PATH)
- {
+ if (destination.length() >= MAX_PATH) {
qWarning() << "Destination path is too long!";
return false;
}
QString argStr;
int argCount = args.count();
- for (int i = 0; i < argCount; i++)
- {
- if (args[i].contains(' '))
- {
+ for (int i = 0; i < argCount; i++) {
+ if (args[i].contains(' ')) {
argStr.append('"').append(args[i]).append('"');
- }
- else
- {
+ } else {
argStr.append(args[i]);
}
- if (i < argCount - 1)
- {
+ if (i < argCount - 1) {
argStr.append(" ");
}
}
- if (argStr.length() >= MAX_PATH)
- {
+ if (argStr.length() >= MAX_PATH) {
qWarning() << "Arguments string is too long!";
return false;
}
@@ -479,8 +931,7 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri
// ...yes, you need to initialize the entire COM stack just to make a shortcut
hres = CoInitialize(nullptr);
- if (FAILED(hres))
- {
+ if (FAILED(hres)) {
qWarning() << "Failed to initialize COM!";
return false;
}
@@ -491,8 +942,7 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri
// create an IShellLink instance - this stores the shortcut's attributes
hres = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLink, (LPVOID*)&psl);
- if (SUCCEEDED(hres))
- {
+ if (SUCCEEDED(hres)) {
wmemset(wsz, 0, MAX_PATH);
target.toWCharArray(wsz);
psl->SetPath(wsz);
@@ -503,10 +953,9 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri
wmemset(wsz, 0, MAX_PATH);
targetInfo.absolutePath().toWCharArray(wsz);
- psl->SetWorkingDirectory(wsz); // "Starts in" attribute
+ psl->SetWorkingDirectory(wsz); // "Starts in" attribute
- if (!icon.isEmpty())
- {
+ if (!icon.isEmpty()) {
wmemset(wsz, 0, MAX_PATH);
icon.toWCharArray(wsz);
psl->SetIconLocation(wsz, 0);
@@ -516,27 +965,21 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri
// this is the interface that will actually let us save the shortcut to disk!
IPersistFile* ppf;
hres = psl->QueryInterface(IID_IPersistFile, (LPVOID*)&ppf);
- if (SUCCEEDED(hres))
- {
+ if (SUCCEEDED(hres)) {
wmemset(wsz, 0, MAX_PATH);
destination.toWCharArray(wsz);
hres = ppf->Save(wsz, TRUE);
- if (FAILED(hres))
- {
+ if (FAILED(hres)) {
qWarning() << "IPresistFile->Save() failed";
qWarning() << "hres = " << hres;
}
ppf->Release();
- }
- else
- {
+ } else {
qWarning() << "Failed to query IPersistFile interface from IShellLink instance";
qWarning() << "hres = " << hres;
}
psl->Release();
- }
- else
- {
+ } else {
qWarning() << "Failed to create IShellLink instance";
qWarning() << "hres = " << hres;
}
@@ -572,4 +1015,494 @@ bool overrideFolder(QString overwritten_path, QString override_path)
return err.value() == 0;
}
+QString getFilesystemTypeName(FilesystemType type)
+{
+ auto iter = s_filesystem_type_names.constFind(type);
+ if (iter != s_filesystem_type_names.constEnd()) {
+ return iter.value().constFirst();
+ }
+ return getFilesystemTypeName(FilesystemType::UNKNOWN);
+}
+
+FilesystemType getFilesystemTypeFuzzy(const QString& name)
+{
+ for (auto iter = s_filesystem_type_names.constBegin(); iter != s_filesystem_type_names.constEnd(); ++iter) {
+ auto fs_names = iter.value();
+ for (auto fs_name : fs_names) {
+ if (name.toUpper().contains(fs_name.toUpper()))
+ return iter.key();
+ }
+ }
+ return FilesystemType::UNKNOWN;
+}
+
+FilesystemType getFilesystemType(const QString& name)
+{
+ for (auto iter = s_filesystem_type_names.constBegin(); iter != s_filesystem_type_names.constEnd(); ++iter) {
+ auto fs_names = iter.value();
+ if (fs_names.contains(name.toUpper()))
+ return iter.key();
+ }
+ return FilesystemType::UNKNOWN;
+}
+
+/**
+ * @brief path to the near ancestor that exists
+ *
+ */
+QString nearestExistentAncestor(const QString& path)
+{
+ if (QFileInfo::exists(path))
+ return path;
+
+ QDir dir(path);
+ if (!dir.makeAbsolute())
+ return {};
+ do {
+ dir.setPath(QDir::cleanPath(dir.filePath(QStringLiteral(".."))));
+ } while (!dir.exists() && !dir.isRoot());
+
+ return dir.exists() ? dir.path() : QString();
+}
+
+/**
+ * @brief colect information about the filesystem under a file
+ *
+ */
+FilesystemInfo statFS(const QString& path)
+{
+ FilesystemInfo info;
+
+ QStorageInfo storage_info(nearestExistentAncestor(path));
+
+ info.fsTypeName = storage_info.fileSystemType();
+
+ info.fsType = getFilesystemTypeFuzzy(info.fsTypeName);
+
+ info.blockSize = storage_info.blockSize();
+ info.bytesAvailable = storage_info.bytesAvailable();
+ info.bytesFree = storage_info.bytesFree();
+ info.bytesTotal = storage_info.bytesTotal();
+
+ info.name = storage_info.name();
+ info.rootPath = storage_info.rootPath();
+
+ return info;
+}
+
+/**
+ * @brief if the Filesystem is reflink/clone capable
+ *
+ */
+bool canCloneOnFS(const QString& path)
+{
+ FilesystemInfo info = statFS(path);
+ return canCloneOnFS(info);
+}
+bool canCloneOnFS(const FilesystemInfo& info)
+{
+ return canCloneOnFS(info.fsType);
+}
+bool canCloneOnFS(FilesystemType type)
+{
+ return s_clone_filesystems.contains(type);
+}
+
+/**
+ * @brief if the Filesystem is reflink/clone capable and both paths are on the same device
+ *
+ */
+bool canClone(const QString& src, const QString& dst)
+{
+ auto srcVInfo = statFS(src);
+ auto dstVInfo = statFS(dst);
+
+ bool sameDevice = srcVInfo.rootPath == dstVInfo.rootPath;
+
+ return sameDevice && canCloneOnFS(srcVInfo) && canCloneOnFS(dstVInfo);
+}
+
+/**
+ * @brief reflink/clones a directory and it's contents from src to dest
+ * @param offset subdirectory form src to copy to dest
+ * @return if there was an error during the filecopy
+ */
+bool clone::operator()(const QString& offset, bool dryRun)
+{
+ if (!canClone(m_src.absolutePath(), m_dst.absolutePath())) {
+ qWarning() << "Can not clone: not same device or not clone/reflink filesystem";
+ qDebug() << "Source path:" << m_src.absolutePath();
+ qDebug() << "Destination path:" << m_dst.absolutePath();
+ emit cloneFailed(m_src.absolutePath(), m_dst.absolutePath());
+ return false;
+ }
+
+ m_cloned = 0; // reset counter
+ m_failedClones.clear();
+
+ auto src = PathCombine(m_src.absolutePath(), offset);
+ auto dst = PathCombine(m_dst.absolutePath(), offset);
+
+ std::error_code err;
+
+ // Function that'll do the actual cloneing
+ auto cloneFile = [&](QString src_path, QString relative_dst_path) {
+ if (m_matcher && (m_matcher->matches(relative_dst_path) != m_whitelist))
+ return;
+
+ auto dst_path = PathCombine(dst, relative_dst_path);
+ if (!dryRun) {
+ ensureFilePathExists(dst_path);
+ clone_file(src_path, dst_path, err);
+ }
+ if (err) {
+ qDebug() << "Failed to clone files: error" << err.value() << "message" << QString::fromStdString(err.message());
+ qDebug() << "Source file:" << src_path;
+ qDebug() << "Destination file:" << dst_path;
+ m_failedClones.append(qMakePair(src_path, dst_path));
+ emit cloneFailed(src_path, dst_path);
+ return;
+ }
+ m_cloned++;
+ emit fileCloned(src_path, dst_path);
+ };
+
+ // We can't use copy_opts::recursive because we need to take into account the
+ // blacklisted paths, so we iterate over the source directory, and if there's no blacklist
+ // match, we copy the file.
+ QDir src_dir(src);
+ QDirIterator source_it(src, QDir::Filter::Files | QDir::Filter::Hidden, QDirIterator::Subdirectories);
+
+ while (source_it.hasNext()) {
+ auto src_path = source_it.next();
+ auto relative_path = src_dir.relativeFilePath(src_path);
+
+ cloneFile(src_path, relative_path);
+ }
+
+ // If the root src is not a directory, the previous iterator won't run.
+ if (!fs::is_directory(StringUtils::toStdString(src)))
+ cloneFile(src, "");
+
+ return err.value() == 0;
+}
+
+/**
+ * @brief clone/reflink file from src to dst
+ *
+ */
+bool clone_file(const QString& src, const QString& dst, std::error_code& ec)
+{
+ auto src_path = StringUtils::toStdString(QDir::toNativeSeparators(QFileInfo(src).absoluteFilePath()));
+ auto dst_path = StringUtils::toStdString(QDir::toNativeSeparators(QFileInfo(dst).absoluteFilePath()));
+
+ FilesystemInfo srcinfo = statFS(src);
+ FilesystemInfo dstinfo = statFS(dst);
+
+ if ((srcinfo.rootPath != dstinfo.rootPath) || (srcinfo.fsType != dstinfo.fsType)) {
+ ec = std::make_error_code(std::errc::not_supported);
+ qWarning() << "reflink/clone must be to the same device and filesystem! src and dst root filesystems do not match.";
+ return false;
+ }
+
+#if defined(Q_OS_WIN)
+
+ if (!win_ioctl_clone(src_path, dst_path, ec)) {
+ qDebug() << "failed win_ioctl_clone";
+ qWarning() << "clone/reflink not supported on windows outside of btrfs or ReFS!";
+ qWarning() << "check out https://github.com/maharmstone/btrfs for btrfs support!";
+ return false;
+ }
+
+#elif defined(Q_OS_LINUX)
+
+ if (!linux_ficlone(src_path, dst_path, ec)) {
+ qDebug() << "failed linux_ficlone:";
+ return false;
+ }
+
+#elif defined(Q_OS_MACOS)
+
+ if (!macos_bsd_clonefile(src_path, dst_path, ec)) {
+ qDebug() << "failed macos_bsd_clonefile:";
+ return false;
+ }
+
+#else
+
+ qWarning() << "clone/reflink not supported! unknown OS";
+ ec = std::make_error_code(std::errc::not_supported);
+ return false;
+
+#endif
+
+ return true;
}
+
+#if defined(Q_OS_WIN)
+
+static long RoundUpToPowerOf2(long originalValue, long roundingMultiplePowerOf2)
+{
+ long mask = roundingMultiplePowerOf2 - 1;
+ return (originalValue + mask) & ~mask;
+}
+
+bool win_ioctl_clone(const std::wstring& src_path, const std::wstring& dst_path, std::error_code& ec)
+{
+ /**
+ * This algorithm inspired from https://github.com/0xbadfca11/reflink
+ * LICENSE MIT
+ *
+ * Additional references
+ * https://learn.microsoft.com/en-us/windows/win32/api/winioctl/ni-winioctl-fsctl_duplicate_extents_to_file
+ * https://github.com/microsoft/CopyOnWrite/blob/main/lib/Windows/WindowsCopyOnWriteFilesystem.cs#L94
+ */
+
+ HANDLE hSourceFile = CreateFileW(src_path.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr);
+ if (hSourceFile == INVALID_HANDLE_VALUE) {
+ ec = std::error_code(GetLastError(), std::system_category());
+ qDebug() << "Failed to open source file" << src_path.c_str();
+ return false;
+ }
+
+ ULONG fs_flags;
+ if (!GetVolumeInformationByHandleW(hSourceFile, nullptr, 0, nullptr, nullptr, &fs_flags, nullptr, 0)) {
+ ec = std::error_code(GetLastError(), std::system_category());
+ qDebug() << "Failed to get Filesystem information for " << src_path.c_str();
+ CloseHandle(hSourceFile);
+ return false;
+ }
+ if (!(fs_flags & FILE_SUPPORTS_BLOCK_REFCOUNTING)) {
+ SetLastError(ERROR_NOT_CAPABLE);
+ ec = std::error_code(GetLastError(), std::system_category());
+ qWarning() << "Filesystem at " << src_path.c_str() << " does not support reflink";
+ CloseHandle(hSourceFile);
+ return false;
+ }
+
+ FILE_END_OF_FILE_INFO sourceFileLength;
+ if (!GetFileSizeEx(hSourceFile, &sourceFileLength.EndOfFile)) {
+ ec = std::error_code(GetLastError(), std::system_category());
+ qDebug() << "Failed to size of source file" << src_path.c_str();
+ CloseHandle(hSourceFile);
+ return false;
+ }
+ FILE_BASIC_INFO sourceFileBasicInfo;
+ if (!GetFileInformationByHandleEx(hSourceFile, FileBasicInfo, &sourceFileBasicInfo, sizeof(sourceFileBasicInfo))) {
+ ec = std::error_code(GetLastError(), std::system_category());
+ qDebug() << "Failed to source file info" << src_path.c_str();
+ CloseHandle(hSourceFile);
+ return false;
+ }
+ ULONG junk;
+ FSCTL_GET_INTEGRITY_INFORMATION_BUFFER sourceFileIntegrity;
+ if (!DeviceIoControl(hSourceFile, FSCTL_GET_INTEGRITY_INFORMATION, nullptr, 0, &sourceFileIntegrity, sizeof(sourceFileIntegrity), &junk,
+ nullptr)) {
+ ec = std::error_code(GetLastError(), std::system_category());
+ qDebug() << "Failed to source file integrity info" << src_path.c_str();
+ CloseHandle(hSourceFile);
+ return false;
+ }
+
+ HANDLE hDestFile = CreateFileW(dst_path.c_str(), GENERIC_READ | GENERIC_WRITE | DELETE, 0, nullptr, CREATE_NEW, 0, hSourceFile);
+
+ if (hDestFile == INVALID_HANDLE_VALUE) {
+ ec = std::error_code(GetLastError(), std::system_category());
+ qDebug() << "Failed to open dest file" << dst_path.c_str();
+ CloseHandle(hSourceFile);
+ return false;
+ }
+ FILE_DISPOSITION_INFO destFileDispose = { TRUE };
+ if (!SetFileInformationByHandle(hDestFile, FileDispositionInfo, &destFileDispose, sizeof(destFileDispose))) {
+ ec = std::error_code(GetLastError(), std::system_category());
+ qDebug() << "Failed to set dest file info" << dst_path.c_str();
+ CloseHandle(hSourceFile);
+ CloseHandle(hDestFile);
+ return false;
+ }
+
+ if (!DeviceIoControl(hDestFile, FSCTL_SET_SPARSE, nullptr, 0, nullptr, 0, &junk, nullptr)) {
+ ec = std::error_code(GetLastError(), std::system_category());
+ qDebug() << "Failed to set dest sparseness" << dst_path.c_str();
+ CloseHandle(hSourceFile);
+ CloseHandle(hDestFile);
+ return false;
+ }
+ FSCTL_SET_INTEGRITY_INFORMATION_BUFFER setDestFileintegrity = { sourceFileIntegrity.ChecksumAlgorithm, sourceFileIntegrity.Reserved,
+ sourceFileIntegrity.Flags };
+ if (!DeviceIoControl(hDestFile, FSCTL_SET_INTEGRITY_INFORMATION, &setDestFileintegrity, sizeof(setDestFileintegrity), nullptr, 0,
+ nullptr, nullptr)) {
+ ec = std::error_code(GetLastError(), std::system_category());
+ qDebug() << "Failed to set dest file integrity info" << dst_path.c_str();
+ CloseHandle(hSourceFile);
+ CloseHandle(hDestFile);
+ return false;
+ }
+ if (!SetFileInformationByHandle(hDestFile, FileEndOfFileInfo, &sourceFileLength, sizeof(sourceFileLength))) {
+ ec = std::error_code(GetLastError(), std::system_category());
+ qDebug() << "Failed to set dest file size" << dst_path.c_str();
+ CloseHandle(hSourceFile);
+ CloseHandle(hDestFile);
+ return false;
+ }
+
+ const LONG64 splitThreshold = (1LL << 32) - sourceFileIntegrity.ClusterSizeInBytes;
+
+ DUPLICATE_EXTENTS_DATA dupExtent;
+ dupExtent.FileHandle = hSourceFile;
+ for (LONG64 offset = 0, remain = RoundUpToPowerOf2(sourceFileLength.EndOfFile.QuadPart, sourceFileIntegrity.ClusterSizeInBytes);
+ remain > 0; offset += splitThreshold, remain -= splitThreshold) {
+ dupExtent.SourceFileOffset.QuadPart = dupExtent.TargetFileOffset.QuadPart = offset;
+ dupExtent.ByteCount.QuadPart = std::min(splitThreshold, remain);
+
+ if (!DeviceIoControl(hDestFile, FSCTL_DUPLICATE_EXTENTS_TO_FILE, &dupExtent, sizeof(dupExtent), nullptr, 0, &junk, nullptr)) {
+ DWORD err = GetLastError();
+ QString additionalMessage;
+ if (err == ERROR_BLOCK_TOO_MANY_REFERENCES) {
+ static const int MaxClonesPerFile = 8175;
+ additionalMessage =
+ QString(
+ " This is ERROR_BLOCK_TOO_MANY_REFERENCES and may mean you have surpassed the maximum "
+ "allowed %1 references for a single file. "
+ "See "
+ "https://docs.microsoft.com/en-us/windows-server/storage/refs/block-cloning#functionality-restrictions-and-remarks")
+ .arg(MaxClonesPerFile);
+ }
+ ec = std::error_code(err, std::system_category());
+ qDebug() << "Failed copy-on-write cloning of" << src_path.c_str() << "to" << dst_path.c_str() << "with error" << err
+ << additionalMessage;
+ CloseHandle(hSourceFile);
+ CloseHandle(hDestFile);
+ return false;
+ }
+ }
+
+ if (!(sourceFileBasicInfo.FileAttributes & FILE_ATTRIBUTE_SPARSE_FILE)) {
+ FILE_SET_SPARSE_BUFFER setDestSparse = { FALSE };
+ if (!DeviceIoControl(hDestFile, FSCTL_SET_SPARSE, &setDestSparse, sizeof(setDestSparse), nullptr, 0, &junk, nullptr)) {
+ qDebug() << "Failed to set dest file sparseness" << dst_path.c_str();
+ CloseHandle(hSourceFile);
+ CloseHandle(hDestFile);
+ return false;
+ }
+ }
+
+ sourceFileBasicInfo.CreationTime.QuadPart = 0;
+ if (!SetFileInformationByHandle(hDestFile, FileBasicInfo, &sourceFileBasicInfo, sizeof(sourceFileBasicInfo))) {
+ qDebug() << "Failed to set dest file creation time" << dst_path.c_str();
+ CloseHandle(hSourceFile);
+ CloseHandle(hDestFile);
+ return false;
+ }
+ if (!FlushFileBuffers(hDestFile)) {
+ qDebug() << "Failed to flush dest file buffer" << dst_path.c_str();
+ CloseHandle(hSourceFile);
+ CloseHandle(hDestFile);
+ return false;
+ }
+ destFileDispose = { FALSE };
+ bool result = !!SetFileInformationByHandle(hDestFile, FileDispositionInfo, &destFileDispose, sizeof(destFileDispose));
+
+ CloseHandle(hSourceFile);
+ CloseHandle(hDestFile);
+
+ return result;
+}
+
+#elif defined(Q_OS_LINUX)
+
+bool linux_ficlone(const std::string& src_path, const std::string& dst_path, std::error_code& ec)
+{
+ // https://man7.org/linux/man-pages/man2/ioctl_ficlone.2.html
+
+ int src_fd = open(src_path.c_str(), O_RDONLY);
+ if (src_fd == -1) {
+ qDebug() << "Failed to open file:" << src_path.c_str();
+ qDebug() << "Error:" << strerror(errno);
+ ec = std::make_error_code(static_cast<std::errc>(errno));
+ return false;
+ }
+ int dst_fd = open(dst_path.c_str(), O_CREAT | O_WRONLY | O_TRUNC, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH);
+ if (dst_fd == -1) {
+ qDebug() << "Failed to open file:" << dst_path.c_str();
+ qDebug() << "Error:" << strerror(errno);
+ ec = std::make_error_code(static_cast<std::errc>(errno));
+ close(src_fd);
+ return false;
+ }
+ // attempt to clone
+ if (ioctl(dst_fd, FICLONE, src_fd) == -1) {
+ qDebug() << "Failed to clone file:" << src_path.c_str() << "to" << dst_path.c_str();
+ qDebug() << "Error:" << strerror(errno);
+ ec = std::make_error_code(static_cast<std::errc>(errno));
+ close(src_fd);
+ close(dst_fd);
+ return false;
+ }
+ if (close(src_fd)) {
+ qDebug() << "Failed to close file:" << src_path.c_str();
+ qDebug() << "Error:" << strerror(errno);
+ }
+ if (close(dst_fd)) {
+ qDebug() << "Failed to close file:" << dst_path.c_str();
+ qDebug() << "Error:" << strerror(errno);
+ }
+ return true;
+}
+
+#elif defined(Q_OS_MACOS)
+
+bool macos_bsd_clonefile(const std::string& src_path, const std::string& dst_path, std::error_code& ec)
+{
+ // clonefile(const char * src, const char * dst, int flags);
+ // https://www.manpagez.com/man/2/clonefile/
+
+ qDebug() << "attempting file clone via clonefile" << src_path.c_str() << "to" << dst_path.c_str();
+ if (clonefile(src_path.c_str(), dst_path.c_str(), 0) == -1) {
+ qDebug() << "Failed to clone file:" << src_path.c_str() << "to" << dst_path.c_str();
+ qDebug() << "Error:" << strerror(errno);
+ ec = std::make_error_code(static_cast<std::errc>(errno));
+ return false;
+ }
+ return true;
+}
+#endif
+
+/**
+ * @brief if the Filesystem is symlink capable
+ *
+ */
+bool canLinkOnFS(const QString& path)
+{
+ FilesystemInfo info = statFS(path);
+ return canLinkOnFS(info);
+}
+bool canLinkOnFS(const FilesystemInfo& info)
+{
+ return canLinkOnFS(info.fsType);
+}
+bool canLinkOnFS(FilesystemType type)
+{
+ return !s_non_link_filesystems.contains(type);
+}
+/**
+ * @brief if the Filesystem is symlink capable on both ends
+ *
+ */
+bool canLink(const QString& src, const QString& dst)
+{
+ return canLinkOnFS(src) && canLinkOnFS(dst);
+}
+
+uintmax_t hardLinkCount(const QString& path)
+{
+ std::error_code err;
+ int count = fs::hard_link_count(StringUtils::toStdString(path), err);
+ if (err) {
+ qWarning() << "Failed to count hard links for" << path << ":" << QString::fromStdString(err.message());
+ count = 0;
+ }
+ return count;
+}
+
+} // namespace FS
diff --git a/launcher/FileSystem.h b/launcher/FileSystem.h
index f083f3c7..f8a82bae 100644
--- a/launcher/FileSystem.h
+++ b/launcher/FileSystem.h
@@ -3,6 +3,7 @@
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
+ * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.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
@@ -39,9 +40,14 @@
#include "Exception.h"
#include "pathmatcher/IPathMatcher.h"
+#include <system_error>
+
#include <QDir>
+#include <QPair>
#include <QFlags>
+#include <QLocalServer>
#include <QObject>
+#include <QThread>
namespace FS {
@@ -77,7 +83,9 @@ bool ensureFilePathExists(QString filenamepath);
*/
bool ensureFolderPathExists(QString filenamepath);
-/// @brief Copies a directory and it's contents from src to dest
+/**
+ * @brief Copies a directory and it's contents from src to dest
+ */
class copy : public QObject {
Q_OBJECT
public:
@@ -105,9 +113,12 @@ class copy : public QObject {
bool operator()(bool dryRun = false) { return operator()(QString(), dryRun); }
int totalCopied() { return m_copied; }
+ int totalFailed() { return m_failedPaths.length(); }
+ QStringList failed() { return m_failedPaths; }
signals:
void fileCopied(const QString& relativeName);
+ void copyFailed(const QString& relativeName);
// TODO: maybe add a "shouldCopy" signal in the future?
private:
@@ -120,15 +131,137 @@ class copy : public QObject {
QDir m_src;
QDir m_dst;
int m_copied;
+ QStringList m_failedPaths;
+};
+
+struct LinkPair {
+ QString src;
+ QString dst;
+};
+
+struct LinkResult {
+ QString src;
+ QString dst;
+ QString err_msg;
+ int err_value;
+};
+
+class ExternalLinkFileProcess : public QThread {
+ Q_OBJECT
+ public:
+ ExternalLinkFileProcess(QString server, bool useHardLinks, QObject* parent = nullptr)
+ : QThread(parent), m_useHardLinks(useHardLinks), m_server(server)
+ {}
+
+ void run() override
+ {
+ runLinkFile();
+ emit processExited();
+ }
+
+ signals:
+ void processExited();
+
+ private:
+ void runLinkFile();
+
+ bool m_useHardLinks = false;
+
+ QString m_server;
+};
+
+/**
+ * @brief links (a file / a directory and it's contents) from src to dest
+ */
+class create_link : public QObject {
+ Q_OBJECT
+ public:
+ create_link(const QList<LinkPair> path_pairs, QObject* parent = nullptr) : QObject(parent) { m_path_pairs.append(path_pairs); }
+ create_link(const QString& src, const QString& dst, QObject* parent = nullptr) : QObject(parent)
+ {
+ LinkPair pair = { src, dst };
+ m_path_pairs.append(pair);
+ }
+ create_link& useHardLinks(const bool useHard)
+ {
+ m_useHardLinks = useHard;
+ return *this;
+ }
+ create_link& matcher(const IPathMatcher* filter)
+ {
+ m_matcher = filter;
+ return *this;
+ }
+ create_link& whitelist(bool whitelist)
+ {
+ m_whitelist = whitelist;
+ return *this;
+ }
+ create_link& linkRecursively(bool recursive)
+ {
+ m_recursive = recursive;
+ return *this;
+ }
+ create_link& setMaxDepth(int depth)
+ {
+ m_max_depth = depth;
+ return *this;
+ }
+ create_link& debug(bool d)
+ {
+ m_debug = d;
+ return *this;
+ }
+
+ std::error_code getOSError() { return m_os_err; }
+
+ bool operator()(bool dryRun = false) { return operator()(QString(), dryRun); }
+
+ int totalLinked() { return m_linked; }
+
+ void runPrivileged() { runPrivileged(QString()); }
+ void runPrivileged(const QString& offset);
+
+ QList<LinkResult> getResults() { return m_path_results; }
+
+ signals:
+ void fileLinked(const QString& srcName, const QString& dstName);
+ void linkFailed(const QString& srcName, const QString& dstName, const QString& err_msg, int err_value);
+ void finished();
+ void finishedPrivileged(bool gotResults);
+
+ private:
+ bool operator()(const QString& offset, bool dryRun = false);
+ void make_link_list(const QString& offset);
+ bool make_links();
+
+ private:
+ bool m_useHardLinks = false;
+ const IPathMatcher* m_matcher = nullptr;
+ bool m_whitelist = false;
+ bool m_recursive = true;
+
+ /// @brief >= -1 = infinite, 0 = link files at src/* to dest/*, 1 = link files at src/*/* to dest/*/*, etc.
+ int m_max_depth = -1;
+
+ QList<LinkPair> m_path_pairs;
+ QList<LinkResult> m_path_results;
+ QList<LinkPair> m_links_to_make;
+
+ int m_linked;
+ bool m_debug = false;
+ std::error_code m_os_err;
+
+ QLocalServer m_linkServer;
};
/**
* @brief moves a file by renaming it
* @param source source file path
* @param dest destination filepath
- *
+ *
*/
-bool move(const QString& source, const QString& dest);
+bool move(const QString& source, const QString& dest);
/**
* Delete a folder recursively
@@ -138,13 +271,30 @@ bool deletePath(QString path);
/**
* Trash a folder / file
*/
-bool trash(QString path, QString *pathInTrash = nullptr);
+bool trash(QString path, QString* pathInTrash = nullptr);
QString PathCombine(const QString& path1, const QString& path2);
QString PathCombine(const QString& path1, const QString& path2, const QString& path3);
QString PathCombine(const QString& path1, const QString& path2, const QString& path3, const QString& path4);
-QString AbsolutePath(QString path);
+QString AbsolutePath(const QString& path);
+
+/**
+ * @brief depth of path. "foo.txt" -> 0 , "bar/foo.txt" -> 1, /baz/bar/foo.txt -> 2, etc.
+ *
+ * @param path path to measure
+ * @return int number of components before base path
+ */
+int pathDepth(const QString& path);
+
+/**
+ * @brief cut off segments of path until it is a max of length depth
+ *
+ * @param path path to truncate
+ * @param depth max depth of new path
+ * @return QString truncated path
+ */
+QString pathTruncate(const QString& path, int depth);
/**
* Resolve an executable
@@ -186,4 +336,198 @@ bool overrideFolder(QString overwritten_path, QString override_path);
* Creates a shortcut to the specified target file at the specified destination path.
*/
bool createShortcut(QString destination, QString target, QStringList args, QString name, QString icon);
-}
+
+enum class FilesystemType {
+ FAT,
+ NTFS,
+ REFS,
+ EXT,
+ EXT_2_OLD,
+ EXT_2_3_4,
+ XFS,
+ BTRFS,
+ NFS,
+ ZFS,
+ APFS,
+ HFS,
+ HFSPLUS,
+ HFSX,
+ FUSEBLK,
+ F2FS,
+ UNKNOWN
+};
+
+/**
+ * @brief Ordered Mapping of enum types to reported filesystem names
+ * this mapping is non exsaustive, it just attempts to capture the filesystems which could be reasonalbly be in use .
+ * all string values are in uppercase, use `QString.toUpper()` or equivalent during lookup.
+ *
+ * QMap is ordered
+ *
+ */
+static const QMap<FilesystemType, QStringList> s_filesystem_type_names = {
+ {FilesystemType::FAT, { "FAT" }},
+ {FilesystemType::NTFS, { "NTFS" }},
+ {FilesystemType::REFS, { "REFS" }},
+ {FilesystemType::EXT_2_OLD, { "EXT_2_OLD", "EXT2_OLD" }},
+ {FilesystemType::EXT_2_3_4, { "EXT2/3/4", "EXT_2_3_4", "EXT2", "EXT3", "EXT4" }},
+ {FilesystemType::EXT, { "EXT" }},
+ {FilesystemType::XFS, { "XFS" }},
+ {FilesystemType::BTRFS, { "BTRFS" }},
+ {FilesystemType::NFS, { "NFS" }},
+ {FilesystemType::ZFS, { "ZFS" }},
+ {FilesystemType::APFS, { "APFS" }},
+ {FilesystemType::HFS, { "HFS" }},
+ {FilesystemType::HFSPLUS, { "HFSPLUS" }},
+ {FilesystemType::HFSX, { "HFSX" }},
+ {FilesystemType::FUSEBLK, { "FUSEBLK" }},
+ {FilesystemType::F2FS, { "F2FS" }},
+ {FilesystemType::UNKNOWN, { "UNKNOWN" }}
+};
+
+/**
+ * @brief Get the string name of Filesystem enum object
+ *
+ * @param type
+ * @return QString
+ */
+QString getFilesystemTypeName(FilesystemType type);
+
+/**
+ * @brief Get the Filesystem enum object from a name
+ * Does a lookup of the type name and returns an exact match
+ *
+ * @param name
+ * @return FilesystemType
+ */
+FilesystemType getFilesystemType(const QString& name);
+
+/**
+ * @brief Get the Filesystem enum object from a name
+ * Does a fuzzy lookup of the type name and returns an apropreate match
+ *
+ * @param name
+ * @return FilesystemType
+ */
+FilesystemType getFilesystemTypeFuzzy(const QString& name);
+
+struct FilesystemInfo {
+ FilesystemType fsType = FilesystemType::UNKNOWN;
+ QString fsTypeName;
+ int blockSize;
+ qint64 bytesAvailable;
+ qint64 bytesFree;
+ qint64 bytesTotal;
+ QString name;
+ QString rootPath;
+};
+
+/**
+ * @brief path to the near ancestor that exists
+ *
+ */
+QString nearestExistentAncestor(const QString& path);
+
+/**
+ * @brief colect information about the filesystem under a file
+ *
+ */
+FilesystemInfo statFS(const QString& path);
+
+static const QList<FilesystemType> s_clone_filesystems = { FilesystemType::BTRFS, FilesystemType::APFS, FilesystemType::ZFS,
+ FilesystemType::XFS, FilesystemType::REFS };
+
+/**
+ * @brief if the Filesystem is reflink/clone capable
+ *
+ */
+bool canCloneOnFS(const QString& path);
+bool canCloneOnFS(const FilesystemInfo& info);
+bool canCloneOnFS(FilesystemType type);
+
+/**
+ * @brief if the Filesystems are reflink/clone capable and both are on the same device
+ *
+ */
+bool canClone(const QString& src, const QString& dst);
+
+/**
+ * @brief Copies a directory and it's contents from src to dest
+ */
+class clone : public QObject {
+ Q_OBJECT
+ public:
+ clone(const QString& src, const QString& dst, QObject* parent = nullptr) : QObject(parent)
+ {
+ m_src.setPath(src);
+ m_dst.setPath(dst);
+ }
+ clone& matcher(const IPathMatcher* filter)
+ {
+ m_matcher = filter;
+ return *this;
+ }
+ clone& whitelist(bool whitelist)
+ {
+ m_whitelist = whitelist;
+ return *this;
+ }
+
+ bool operator()(bool dryRun = false) { return operator()(QString(), dryRun); }
+
+ int totalCloned() { return m_cloned; }
+ int totalFailed() { return m_failedClones.length(); }
+
+ QList<QPair<QString, QString>> failed() { return m_failedClones; }
+
+ signals:
+ void fileCloned(const QString& src, const QString& dst);
+ void cloneFailed(const QString& src, const QString& dst);
+
+ private:
+ bool operator()(const QString& offset, bool dryRun = false);
+
+ private:
+ const IPathMatcher* m_matcher = nullptr;
+ bool m_whitelist = false;
+ QDir m_src;
+ QDir m_dst;
+ int m_cloned;
+ QList<QPair<QString, QString>> m_failedClones;
+};
+
+/**
+ * @brief clone/reflink file from src to dst
+ *
+ */
+bool clone_file(const QString& src, const QString& dst, std::error_code& ec);
+
+#if defined(Q_OS_WIN)
+bool win_ioctl_clone(const std::wstring& src_path, const std::wstring& dst_path, std::error_code& ec);
+#elif defined(Q_OS_LINUX)
+bool linux_ficlone(const std::string& src_path, const std::string& dst_path, std::error_code& ec);
+#elif defined(Q_OS_MACOS) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
+bool macos_bsd_clonefile(const std::string& src_path, const std::string& dst_path, std::error_code& ec);
+#endif
+
+static const QList<FilesystemType> s_non_link_filesystems = {
+ FilesystemType::FAT,
+};
+
+/**
+ * @brief if the Filesystem is symlink capable
+ *
+ */
+bool canLinkOnFS(const QString& path);
+bool canLinkOnFS(const FilesystemInfo& info);
+bool canLinkOnFS(FilesystemType type);
+
+/**
+ * @brief if the Filesystem is symlink capable on both ends
+ *
+ */
+bool canLink(const QString& src, const QString& dst);
+
+uintmax_t hardLinkCount(const QString& path);
+
+} // namespace FS
diff --git a/launcher/InstanceCopyPrefs.cpp b/launcher/InstanceCopyPrefs.cpp
index 7b93a516..0650002b 100644
--- a/launcher/InstanceCopyPrefs.cpp
+++ b/launcher/InstanceCopyPrefs.cpp
@@ -16,9 +16,14 @@ bool InstanceCopyPrefs::allTrue() const
copyScreenshots;
}
+
// Returns a single RegEx string of the selected folders/files to filter out (ex: ".minecraft/saves|.minecraft/server.dat")
QString InstanceCopyPrefs::getSelectedFiltersAsRegex() const
{
+ return getSelectedFiltersAsRegex({});
+}
+QString InstanceCopyPrefs::getSelectedFiltersAsRegex(const QStringList& additionalFilters) const
+{
QStringList filters;
if(!copySaves)
@@ -42,6 +47,10 @@ QString InstanceCopyPrefs::getSelectedFiltersAsRegex() const
if(!copyScreenshots)
filters << "screenshots";
+ for (auto filter : additionalFilters) {
+ filters << filter;
+ }
+
// If we have any filters to add, join them as a single regex string to return:
if (!filters.isEmpty()) {
const QString MC_ROOT = "[.]?minecraft/";
@@ -93,6 +102,31 @@ bool InstanceCopyPrefs::isCopyScreenshotsEnabled() const
return copyScreenshots;
}
+bool InstanceCopyPrefs::isUseSymLinksEnabled() const
+{
+ return useSymLinks;
+}
+
+bool InstanceCopyPrefs::isUseHardLinksEnabled() const
+{
+ return useHardLinks;
+}
+
+bool InstanceCopyPrefs::isLinkRecursivelyEnabled() const
+{
+ return linkRecursively;
+}
+
+bool InstanceCopyPrefs::isDontLinkSavesEnabled() const
+{
+ return dontLinkSaves;
+}
+
+bool InstanceCopyPrefs::isUseCloneEnabled() const
+{
+ return useClone;
+}
+
// ======= Setters =======
void InstanceCopyPrefs::enableCopySaves(bool b)
{
@@ -133,3 +167,28 @@ void InstanceCopyPrefs::enableCopyScreenshots(bool b)
{
copyScreenshots = b;
}
+
+void InstanceCopyPrefs::enableUseSymLinks(bool b)
+{
+ useSymLinks = b;
+}
+
+void InstanceCopyPrefs::enableLinkRecursively(bool b)
+{
+ linkRecursively = b;
+}
+
+void InstanceCopyPrefs::enableUseHardLinks(bool b)
+{
+ useHardLinks = b;
+}
+
+void InstanceCopyPrefs::enableDontLinkSaves(bool b)
+{
+ dontLinkSaves = b;
+}
+
+void InstanceCopyPrefs::enableUseClone(bool b)
+{
+ useClone = b;
+} \ No newline at end of file
diff --git a/launcher/InstanceCopyPrefs.h b/launcher/InstanceCopyPrefs.h
index 6988b2df..c7bde068 100644
--- a/launcher/InstanceCopyPrefs.h
+++ b/launcher/InstanceCopyPrefs.h
@@ -10,6 +10,7 @@ struct InstanceCopyPrefs {
public:
[[nodiscard]] bool allTrue() const;
[[nodiscard]] QString getSelectedFiltersAsRegex() const;
+ [[nodiscard]] QString getSelectedFiltersAsRegex(const QStringList& additionalFilters) const;
// Getters
[[nodiscard]] bool isCopySavesEnabled() const;
[[nodiscard]] bool isKeepPlaytimeEnabled() const;
@@ -19,6 +20,11 @@ struct InstanceCopyPrefs {
[[nodiscard]] bool isCopyServersEnabled() const;
[[nodiscard]] bool isCopyModsEnabled() const;
[[nodiscard]] bool isCopyScreenshotsEnabled() const;
+ [[nodiscard]] bool isUseSymLinksEnabled() const;
+ [[nodiscard]] bool isLinkRecursivelyEnabled() const;
+ [[nodiscard]] bool isUseHardLinksEnabled() const;
+ [[nodiscard]] bool isDontLinkSavesEnabled() const;
+ [[nodiscard]] bool isUseCloneEnabled() const;
// Setters
void enableCopySaves(bool b);
void enableKeepPlaytime(bool b);
@@ -28,6 +34,11 @@ struct InstanceCopyPrefs {
void enableCopyServers(bool b);
void enableCopyMods(bool b);
void enableCopyScreenshots(bool b);
+ void enableUseSymLinks(bool b);
+ void enableLinkRecursively(bool b);
+ void enableUseHardLinks(bool b);
+ void enableDontLinkSaves(bool b);
+ void enableUseClone(bool b);
protected: // data
bool copySaves = true;
@@ -38,4 +49,9 @@ struct InstanceCopyPrefs {
bool copyServers = true;
bool copyMods = true;
bool copyScreenshots = true;
+ bool useSymLinks = false;
+ bool linkRecursively = false;
+ bool useHardLinks = false;
+ bool dontLinkSaves = false;
+ bool useClone = false;
};
diff --git a/launcher/InstanceCopyTask.cpp b/launcher/InstanceCopyTask.cpp
index 188d163b..57a3143a 100644
--- a/launcher/InstanceCopyTask.cpp
+++ b/launcher/InstanceCopyTask.cpp
@@ -1,18 +1,31 @@
#include "InstanceCopyTask.h"
-#include "settings/INISettingsObject.h"
+#include <QDebug>
+#include <QtConcurrentRun>
#include "FileSystem.h"
#include "NullInstance.h"
#include "pathmatcher/RegexpMatcher.h"
-#include <QtConcurrentRun>
+#include "settings/INISettingsObject.h"
InstanceCopyTask::InstanceCopyTask(InstancePtr origInstance, const InstanceCopyPrefs& prefs)
{
m_origInstance = origInstance;
m_keepPlaytime = prefs.isKeepPlaytimeEnabled();
+ m_useLinks = prefs.isUseSymLinksEnabled();
+ m_linkRecursively = prefs.isLinkRecursivelyEnabled();
+ m_useHardLinks = prefs.isLinkRecursivelyEnabled() && prefs.isUseHardLinksEnabled();
+ m_copySaves = prefs.isLinkRecursivelyEnabled() && prefs.isDontLinkSavesEnabled() && prefs.isCopySavesEnabled();
+ m_useClone = prefs.isUseCloneEnabled();
QString filters = prefs.getSelectedFiltersAsRegex();
- if (!filters.isEmpty())
- {
+ if (m_useLinks || m_useHardLinks) {
+ if (!filters.isEmpty())
+ filters += "|";
+ filters += "instance.cfg";
+ }
+
+ qDebug() << "CopyFilters:" << filters;
+
+ if (!filters.isEmpty()) {
// Set regex filter:
// FIXME: get this from the original instance type...
auto matcherReal = new RegexpMatcher(filters);
@@ -25,11 +38,87 @@ void InstanceCopyTask::executeTask()
{
setStatus(tr("Copying instance %1").arg(m_origInstance->name()));
- m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this]{
- FS::copy folderCopy(m_origInstance->instanceRoot(), m_stagingPath);
- folderCopy.followSymlinks(false).matcher(m_matcher.get());
+ auto copySaves = [&]() {
+ QFileInfo mcDir(FS::PathCombine(m_stagingPath, "minecraft"));
+ QFileInfo dotMCDir(FS::PathCombine(m_stagingPath, ".minecraft"));
+
+ QString staging_mc_dir;
+ if (mcDir.exists() && !dotMCDir.exists())
+ staging_mc_dir = mcDir.filePath();
+ else
+ staging_mc_dir = dotMCDir.filePath();
+
+ FS::copy savesCopy(FS::PathCombine(m_origInstance->gameRoot(), "saves"), FS::PathCombine(staging_mc_dir, "saves"));
+ savesCopy.followSymlinks(true);
+
+ return savesCopy();
+ };
+
+ m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this, copySaves] {
+ if (m_useClone) {
+ FS::clone folderClone(m_origInstance->instanceRoot(), m_stagingPath);
+ folderClone.matcher(m_matcher.get());
+
+ return folderClone();
+ } else if (m_useLinks || m_useHardLinks) {
+ FS::create_link folderLink(m_origInstance->instanceRoot(), m_stagingPath);
+ int depth = m_linkRecursively ? -1 : 0; // we need to at least link the top level instead of the instance folder
+ folderLink.linkRecursively(true).setMaxDepth(depth).useHardLinks(m_useHardLinks).matcher(m_matcher.get());
+
+ bool there_were_errors = false;
+
+ if (!folderLink()) {
+#if defined Q_OS_WIN32
+ if (!m_useHardLinks) {
+ qDebug() << "EXPECTED: Link failure, Windows requires permissions for symlinks";
- return folderCopy();
+ qDebug() << "attempting to run with privelage";
+
+ QEventLoop loop;
+ bool got_priv_results = false;
+
+ connect(&folderLink, &FS::create_link::finishedPrivileged, this, [&](bool gotResults) {
+ if (!gotResults) {
+ qDebug() << "Privileged run exited without results!";
+ }
+ got_priv_results = gotResults;
+ loop.quit();
+ });
+ folderLink.runPrivileged();
+
+ loop.exec(); // wait for the finished signal
+
+ for (auto result : folderLink.getResults()) {
+ if (result.err_value != 0) {
+ there_were_errors = true;
+ }
+ }
+
+ if (m_copySaves) {
+ there_were_errors |= !copySaves();
+ }
+
+ return got_priv_results && !there_were_errors;
+ } else {
+ qDebug() << "Link Failed!" << folderLink.getOSError().value() << folderLink.getOSError().message().c_str();
+ }
+#else
+ qDebug() << "Link Failed!" << folderLink.getOSError().value() << folderLink.getOSError().message().c_str();
+#endif
+ return false;
+ }
+
+ if (m_copySaves) {
+ there_were_errors |= !copySaves();
+ }
+
+ return !there_were_errors;
+ } else {
+ FS::copy folderCopy(m_origInstance->instanceRoot(), m_stagingPath);
+ folderCopy.followSymlinks(false).matcher(m_matcher.get());
+
+ return folderCopy();
+ }
});
connect(&m_copyFutureWatcher, &QFutureWatcher<bool>::finished, this, &InstanceCopyTask::copyFinished);
connect(&m_copyFutureWatcher, &QFutureWatcher<bool>::canceled, this, &InstanceCopyTask::copyAborted);
@@ -39,20 +128,40 @@ void InstanceCopyTask::executeTask()
void InstanceCopyTask::copyFinished()
{
auto successful = m_copyFuture.result();
- if(!successful)
- {
+ if (!successful) {
emitFailed(tr("Instance folder copy failed."));
return;
}
+
// FIXME: shouldn't this be able to report errors?
auto instanceSettings = std::make_shared<INISettingsObject>(FS::PathCombine(m_stagingPath, "instance.cfg"));
InstancePtr inst(new NullInstance(m_globalSettings, instanceSettings, m_stagingPath));
inst->setName(name());
inst->setIconKey(m_instIcon);
- if(!m_keepPlaytime) {
+ if (!m_keepPlaytime) {
inst->resetTimePlayed();
}
+ if (m_useLinks)
+ inst->addLinkedInstanceId(m_origInstance->id());
+ if (m_useLinks) {
+ auto allowed_symlinks_file = QFileInfo(FS::PathCombine(inst->gameRoot(), "allowed_symlinks.txt"));
+
+ QByteArray allowed_symlinks;
+ if (allowed_symlinks_file.exists()) {
+ allowed_symlinks.append(FS::read(allowed_symlinks_file.filePath()));
+ if (allowed_symlinks.right(1) != "\n")
+ allowed_symlinks.append("\n"); // we want to be on a new line
+ }
+ allowed_symlinks.append(m_origInstance->gameRoot().toUtf8());
+ allowed_symlinks.append("\n");
+ if (allowed_symlinks_file.isSymLink())
+ FS::deletePath(allowed_symlinks_file
+ .filePath()); // we dont want to modify the original. also make sure the resulting file is not itself a link.
+
+ FS::write(allowed_symlinks_file.filePath(), allowed_symlinks);
+ }
+
emitSucceeded();
}
diff --git a/launcher/InstanceCopyTask.h b/launcher/InstanceCopyTask.h
index 1f29b854..aea9d99a 100644
--- a/launcher/InstanceCopyTask.h
+++ b/launcher/InstanceCopyTask.h
@@ -30,4 +30,9 @@ private:
QFutureWatcher<bool> m_copyFutureWatcher;
std::unique_ptr<IPathMatcher> m_matcher;
bool m_keepPlaytime;
+ bool m_useLinks = false;
+ bool m_useHardLinks = false;
+ bool m_copySaves = false;
+ bool m_linkRecursively = false;
+ bool m_useClone = false;
};
diff --git a/launcher/InstanceCreationTask.h b/launcher/InstanceCreationTask.h
index 03ee1a7a..380fdf8a 100644
--- a/launcher/InstanceCreationTask.h
+++ b/launcher/InstanceCreationTask.h
@@ -34,7 +34,7 @@ class InstanceCreationTask : public InstanceTask {
QString getError() const { return m_error_message; }
protected:
- void setError(QString message) { m_error_message = message; };
+ void setError(const QString& message) { m_error_message = message; };
protected:
bool m_abort = false;
diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp
index 080828a8..352848f0 100644
--- a/launcher/InstanceImportTask.cpp
+++ b/launcher/InstanceImportTask.cpp
@@ -41,6 +41,7 @@
#include "MMCZip.h"
#include "NullInstance.h"
+#include "QObjectPtr.h"
#include "icons/IconList.h"
#include "icons/IconUtils.h"
@@ -98,6 +99,7 @@ void InstanceImportTask::executeTask()
connect(m_filesNetJob.get(), &NetJob::succeeded, this, &InstanceImportTask::downloadSucceeded);
connect(m_filesNetJob.get(), &NetJob::progress, this, &InstanceImportTask::downloadProgressChanged);
+ connect(m_filesNetJob.get(), &NetJob::stepProgress, this, &InstanceImportTask::propogateStepProgress);
connect(m_filesNetJob.get(), &NetJob::failed, this, &InstanceImportTask::downloadFailed);
connect(m_filesNetJob.get(), &NetJob::aborted, this, &InstanceImportTask::downloadAborted);
@@ -259,7 +261,7 @@ void InstanceImportTask::extractFinished()
void InstanceImportTask::processFlame()
{
- FlameCreationTask* inst_creation_task = nullptr;
+ shared_qobject_ptr<FlameCreationTask> inst_creation_task = nullptr;
if (!m_extra_info.isEmpty()) {
auto pack_id_it = m_extra_info.constFind("pack_id");
Q_ASSERT(pack_id_it != m_extra_info.constEnd());
@@ -274,10 +276,10 @@ void InstanceImportTask::processFlame()
if (original_instance_id_it != m_extra_info.constEnd())
original_instance_id = original_instance_id_it.value();
- inst_creation_task = new FlameCreationTask(m_stagingPath, m_globalSettings, m_parent, pack_id, pack_version_id, original_instance_id);
+ inst_creation_task = makeShared<FlameCreationTask>(m_stagingPath, m_globalSettings, m_parent, pack_id, pack_version_id, original_instance_id);
} else {
// FIXME: Find a way to get IDs in directly imported ZIPs
- inst_creation_task = new FlameCreationTask(m_stagingPath, m_globalSettings, m_parent, {}, {});
+ inst_creation_task = makeShared<FlameCreationTask>(m_stagingPath, m_globalSettings, m_parent, QString(), QString());
}
inst_creation_task->setName(*this);
@@ -285,18 +287,19 @@ void InstanceImportTask::processFlame()
inst_creation_task->setGroup(m_instGroup);
inst_creation_task->setConfirmUpdate(shouldConfirmUpdate());
- connect(inst_creation_task, &Task::succeeded, this, [this, inst_creation_task] {
+ connect(inst_creation_task.get(), &Task::succeeded, this, [this, inst_creation_task] {
setOverride(inst_creation_task->shouldOverride(), inst_creation_task->originalInstanceID());
emitSucceeded();
});
- connect(inst_creation_task, &Task::failed, this, &InstanceImportTask::emitFailed);
- connect(inst_creation_task, &Task::progress, this, &InstanceImportTask::setProgress);
- connect(inst_creation_task, &Task::status, this, &InstanceImportTask::setStatus);
- connect(inst_creation_task, &Task::finished, inst_creation_task, &InstanceCreationTask::deleteLater);
+ connect(inst_creation_task.get(), &Task::failed, this, &InstanceImportTask::emitFailed);
+ connect(inst_creation_task.get(), &Task::progress, this, &InstanceImportTask::setProgress);
+ connect(inst_creation_task.get(), &Task::stepProgress, this, &InstanceImportTask::propogateStepProgress);
+ connect(inst_creation_task.get(), &Task::status, this, &InstanceImportTask::setStatus);
+ connect(inst_creation_task.get(), &Task::details, this, &InstanceImportTask::setDetails);
- connect(this, &Task::aborted, inst_creation_task, &InstanceCreationTask::abort);
- connect(inst_creation_task, &Task::aborted, this, &Task::abort);
- connect(inst_creation_task, &Task::abortStatusChanged, this, &Task::setAbortable);
+ connect(this, &Task::aborted, inst_creation_task.get(), &InstanceCreationTask::abort);
+ connect(inst_creation_task.get(), &Task::aborted, this, &Task::abort);
+ connect(inst_creation_task.get(), &Task::abortStatusChanged, this, &Task::setAbortable);
inst_creation_task->start();
}
@@ -382,7 +385,9 @@ void InstanceImportTask::processModrinth()
});
connect(inst_creation_task, &Task::failed, this, &InstanceImportTask::emitFailed);
connect(inst_creation_task, &Task::progress, this, &InstanceImportTask::setProgress);
+ connect(inst_creation_task, &Task::stepProgress, this, &InstanceImportTask::propogateStepProgress);
connect(inst_creation_task, &Task::status, this, &InstanceImportTask::setStatus);
+ connect(inst_creation_task, &Task::details, this, &InstanceImportTask::setDetails);
connect(inst_creation_task, &Task::finished, inst_creation_task, &InstanceCreationTask::deleteLater);
connect(this, &Task::aborted, inst_creation_task, &InstanceCreationTask::abort);
diff --git a/launcher/InstanceList.cpp b/launcher/InstanceList.cpp
index 68e3e92c..b4c520cd 100644
--- a/launcher/InstanceList.cpp
+++ b/launcher/InstanceList.cpp
@@ -129,6 +129,16 @@ QMimeData* InstanceList::mimeData(const QModelIndexList& indexes) const
return mimeData;
}
+QStringList InstanceList::getLinkedInstancesById(const QString &id) const
+{
+ QStringList linkedInstances;
+ for (auto inst : m_instances) {
+ if (inst->isLinkedToInstanceId(id))
+ linkedInstances.append(inst->id());
+ }
+ return linkedInstances;
+}
+
int InstanceList::rowCount(const QModelIndex& parent) const
{
Q_UNUSED(parent);
@@ -787,7 +797,9 @@ class InstanceStaging : public Task {
connect(child, &Task::aborted, this, &InstanceStaging::childAborted);
connect(child, &Task::abortStatusChanged, this, &InstanceStaging::setAbortable);
connect(child, &Task::status, this, &InstanceStaging::setStatus);
+ connect(child, &Task::details, this, &InstanceStaging::setDetails);
connect(child, &Task::progress, this, &InstanceStaging::setProgress);
+ connect(child, &Task::stepProgress, this, &InstanceStaging::propogateStepProgress);
connect(&m_backoffTimer, &QTimer::timeout, this, &InstanceStaging::childSucceded);
}
@@ -865,7 +877,7 @@ Task* InstanceList::wrapInstanceTask(InstanceTask* task)
QString InstanceList::getStagedInstancePath()
{
- QString key = QUuid::createUuid().toString();
+ QString key = QUuid::createUuid().toString(QUuid::WithoutBraces);
QString tempDir = ".LAUNCHER_TEMP/";
QString relPath = FS::PathCombine(tempDir, key);
QDir rootPath(m_instDir);
diff --git a/launcher/InstanceList.h b/launcher/InstanceList.h
index edacba3c..48bede07 100644
--- a/launcher/InstanceList.h
+++ b/launcher/InstanceList.h
@@ -154,6 +154,8 @@ public:
QStringList mimeTypes() const override;
QMimeData *mimeData(const QModelIndexList &indexes) const override;
+ QStringList getLinkedInstancesById(const QString &id) const;
+
signals:
void dataIsInvalid();
void instancesChanged();
diff --git a/launcher/InstanceTask.cpp b/launcher/InstanceTask.cpp
index 06682782..b16a40ba 100644
--- a/launcher/InstanceTask.cpp
+++ b/launcher/InstanceTask.cpp
@@ -45,7 +45,10 @@ QString InstanceName::name() const
{
if (!m_modified_name.isEmpty())
return modifiedName();
- return QString("%1 %2").arg(m_original_name, m_original_version);
+ if (!m_original_version.isEmpty())
+ return QString("%1 %2").arg(m_original_name, m_original_version);
+
+ return m_original_name;
}
QString InstanceName::originalName() const
diff --git a/launcher/JavaCommon.cpp b/launcher/JavaCommon.cpp
index 52cc868a..e29e2270 100644
--- a/launcher/JavaCommon.cpp
+++ b/launcher/JavaCommon.cpp
@@ -122,8 +122,7 @@ void JavaCommon::TestCheck::run()
return;
}
checker.reset(new JavaChecker());
- connect(checker.get(), SIGNAL(checkFinished(JavaCheckResult)), this,
- SLOT(checkFinished(JavaCheckResult)));
+ connect(checker.get(), &JavaChecker::checkFinished, this, &JavaCommon::TestCheck::checkFinished);
checker->m_path = m_path;
checker->performCheck();
}
@@ -137,8 +136,7 @@ void JavaCommon::TestCheck::checkFinished(JavaCheckResult result)
return;
}
checker.reset(new JavaChecker());
- connect(checker.get(), SIGNAL(checkFinished(JavaCheckResult)), this,
- SLOT(checkFinishedWithArgs(JavaCheckResult)));
+ connect(checker.get(), &JavaChecker::checkFinished, this, &JavaCommon::TestCheck::checkFinishedWithArgs);
checker->m_path = m_path;
checker->m_args = m_args;
checker->m_minMem = m_minMem;
diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp
index 070ee283..5d84b3bf 100644
--- a/launcher/LaunchController.cpp
+++ b/launcher/LaunchController.cpp
@@ -187,8 +187,8 @@ void LaunchController::login() {
switch(m_accountToUse->accountState()) {
case AccountState::Offline: {
m_session->wants_online = false;
- // NOTE: fallthrough is intentional
}
+ /* fallthrough */
case AccountState::Online: {
if(!m_session->wants_online) {
// we ask the user for a player name
@@ -267,8 +267,8 @@ void LaunchController::login() {
// This means some sort of soft error that we can fix with a refresh ... so let's refresh.
case AccountState::Unchecked: {
m_accountToUse->refresh();
- // NOTE: fallthrough intentional
}
+ /* fallthrough */
case AccountState::Working: {
// refresh is in progress, we need to wait for it to finish to proceed.
ProgressDialog progDialog(m_parentWidget);
diff --git a/launcher/LoggedProcess.cpp b/launcher/LoggedProcess.cpp
index c8d5c34e..d70f6d00 100644
--- a/launcher/LoggedProcess.cpp
+++ b/launcher/LoggedProcess.cpp
@@ -44,12 +44,8 @@ LoggedProcess::LoggedProcess(QObject *parent) : QProcess(parent)
// QProcess has a strange interface... let's map a lot of those into a few.
connect(this, &QProcess::readyReadStandardOutput, this, &LoggedProcess::on_stdOut);
connect(this, &QProcess::readyReadStandardError, this, &LoggedProcess::on_stdErr);
- connect(this, SIGNAL(finished(int,QProcess::ExitStatus)), SLOT(on_exit(int,QProcess::ExitStatus)));
-#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
- connect(this, SIGNAL(errorOccurred(QProcess::ProcessError)), this, SLOT(on_error(QProcess::ProcessError)));
-#else
- connect(this, SIGNAL(error(QProcess::ProcessError)), this, SLOT(on_error(QProcess::ProcessError)));
-#endif
+ connect(this, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this, &LoggedProcess::on_exit);
+ connect(this, &QProcess::errorOccurred, this, &LoggedProcess::on_error);
connect(this, &QProcess::stateChanged, this, &LoggedProcess::on_stateChange);
}
diff --git a/launcher/MMCTime.cpp b/launcher/MMCTime.cpp
index 70bc4135..1702ec06 100644
--- a/launcher/MMCTime.cpp
+++ b/launcher/MMCTime.cpp
@@ -18,6 +18,8 @@
#include <MMCTime.h>
#include <QObject>
+#include <QDateTime>
+#include <QTextStream>
QString Time::prettifyDuration(int64_t duration) {
int seconds = (int) (duration % 60);
@@ -36,3 +38,65 @@ QString Time::prettifyDuration(int64_t duration) {
}
return QObject::tr("%1d %2h %3min").arg(days).arg(hours).arg(minutes);
}
+
+QString Time::humanReadableDuration(double duration, int precision) {
+
+ using days = std::chrono::duration<int, std::ratio<86400>>;
+
+ QString outStr;
+ QTextStream os(&outStr);
+
+ bool neg = false;
+ if (duration < 0) {
+ neg = true; // flag
+ duration *= -1; // invert
+ }
+
+ auto std_duration = std::chrono::duration<double>(duration);
+ auto d = std::chrono::duration_cast<days>(std_duration);
+ std_duration -= d;
+ auto h = std::chrono::duration_cast<std::chrono::hours>(std_duration);
+ std_duration -= h;
+ auto m = std::chrono::duration_cast<std::chrono::minutes>(std_duration);
+ std_duration -= m;
+ auto s = std::chrono::duration_cast<std::chrono::seconds>(std_duration);
+ std_duration -= s;
+ auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(std_duration);
+
+ auto dc = d.count();
+ auto hc = h.count();
+ auto mc = m.count();
+ auto sc = s.count();
+ auto msc = ms.count();
+
+ if (neg) {
+ os << '-';
+ }
+ if (dc) {
+ os << dc << QObject::tr("days");
+ }
+ if (hc) {
+ if (dc)
+ os << " ";
+ os << qSetFieldWidth(2) << hc << QObject::tr("h"); // hours
+ }
+ if (mc) {
+ if (dc || hc)
+ os << " ";
+ os << qSetFieldWidth(2) << mc << QObject::tr("m"); // minutes
+ }
+ if (dc || hc || mc || sc) {
+ if (dc || hc || mc)
+ os << " ";
+ os << qSetFieldWidth(2) << sc << QObject::tr("s"); // seconds
+ }
+ if ((msc && (precision > 0)) || !(dc || hc || mc || sc)) {
+ if (dc || hc || mc || sc)
+ os << " ";
+ os << qSetFieldWidth(0) << qSetRealNumberPrecision(precision) << msc << QObject::tr("ms"); // miliseconds
+ }
+
+ os.flush();
+
+ return outStr;
+} \ No newline at end of file
diff --git a/launcher/MMCTime.h b/launcher/MMCTime.h
index 10ff2ffe..6a5780b4 100644
--- a/launcher/MMCTime.h
+++ b/launcher/MMCTime.h
@@ -22,4 +22,13 @@ namespace Time {
QString prettifyDuration(int64_t duration);
+/**
+ * @brief Returns a string with short form time duration ie. `2days 1h3m4s56.0ms`.
+ * miliseconds are only included if `precision` is greater than 0.
+ *
+ * @param duration a number of seconds as floating point
+ * @param precision number of decmial points to display on fractons of a second, defualts to 0.
+ * @return QString
+ */
+QString humanReadableDuration(double duration, int precision = 0);
}
diff --git a/launcher/MMCZip.cpp b/launcher/MMCZip.cpp
index 1eda43fe..1a336375 100644
--- a/launcher/MMCZip.cpp
+++ b/launcher/MMCZip.cpp
@@ -94,20 +94,28 @@ bool MMCZip::mergeZipFiles(QuaZip *into, QFileInfo from, QSet<QString> &containe
return true;
}
-bool MMCZip::compressDirFiles(QuaZip *zip, QString dir, QFileInfoList files)
+bool MMCZip::compressDirFiles(QuaZip *zip, QString dir, QFileInfoList files, bool followSymlinks)
{
QDir directory(dir);
if (!directory.exists()) return false;
for (auto e : files) {
auto filePath = directory.relativeFilePath(e.absoluteFilePath());
- if( !JlCompress::compressFile(zip, e.absoluteFilePath(), filePath)) return false;
+ auto srcPath = e.absoluteFilePath();
+ if (followSymlinks) {
+ if (e.isSymLink()) {
+ srcPath = e.symLinkTarget();
+ } else {
+ srcPath = e.canonicalFilePath();
+ }
+ }
+ if( !JlCompress::compressFile(zip, srcPath, filePath)) return false;
}
return true;
}
-bool MMCZip::compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files)
+bool MMCZip::compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files, bool followSymlinks)
{
QuaZip zip(fileCompressed);
QDir().mkpath(QFileInfo(fileCompressed).absolutePath());
@@ -116,7 +124,7 @@ bool MMCZip::compressDirFiles(QString fileCompressed, QString dir, QFileInfoList
return false;
}
- auto result = compressDirFiles(&zip, dir, files);
+ auto result = compressDirFiles(&zip, dir, files, followSymlinks);
zip.close();
if(zip.getZipError()!=0) {
diff --git a/launcher/MMCZip.h b/launcher/MMCZip.h
index 81f9cb90..2a78f830 100644
--- a/launcher/MMCZip.h
+++ b/launcher/MMCZip.h
@@ -59,18 +59,20 @@ namespace MMCZip
* \param zip target archive
* \param dir directory that will be compressed (to compress with relative paths)
* \param files list of files to compress
+ * \param followSymlinks should follow symlinks when compressing file data
* \return true for success or false for failure
*/
- bool compressDirFiles(QuaZip *zip, QString dir, QFileInfoList files);
+ bool compressDirFiles(QuaZip *zip, QString dir, QFileInfoList files, bool followSymlinks = false);
/**
* Compress directory, by providing a list of files to compress
* \param fileCompressed target archive file
* \param dir directory that will be compressed (to compress with relative paths)
* \param files list of files to compress
+ * \param followSymlinks should follow symlinks when compressing file data
* \return true for success or false for failure
*/
- bool compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files);
+ bool compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files, bool followSymlinks = false);
/**
* take a source jar, add mods to it, resulting in target jar
diff --git a/launcher/MTPixmapCache.h b/launcher/MTPixmapCache.h
index 57847a0e..65cbe032 100644
--- a/launcher/MTPixmapCache.h
+++ b/launcher/MTPixmapCache.h
@@ -3,6 +3,8 @@
#include <QCoreApplication>
#include <QPixmapCache>
#include <QThread>
+#include <QTime>
+#include <QDebug>
#define GET_TYPE() \
Qt::ConnectionType type; \
@@ -60,6 +62,8 @@ class PixmapCache final : public QObject {
DEFINE_FUNC_ONE_PARAM(remove, bool, const QPixmapCache::Key&)
DEFINE_FUNC_TWO_PARAM(replace, bool, const QPixmapCache::Key&, const QPixmap&)
DEFINE_FUNC_ONE_PARAM(setCacheLimit, bool, int)
+ DEFINE_FUNC_NO_PARAM(markCacheMissByEviciton, bool)
+ DEFINE_FUNC_ONE_PARAM(setFastEvictionThreshold, bool, int)
// NOTE: Every function returns something non-void to simplify the macros.
private slots:
@@ -90,6 +94,43 @@ class PixmapCache final : public QObject {
return true;
}
+ /**
+ * Mark that a cache miss occurred because of a eviction if too many of these occur too fast the cache size is increased
+ * @return if the cache size was increased
+ */
+ bool _markCacheMissByEviciton()
+ {
+ auto now = QTime::currentTime();
+ if (!m_last_cache_miss_by_eviciton.isNull()) {
+ auto diff = m_last_cache_miss_by_eviciton.msecsTo(now);
+ if (diff < 1000) { // less than a second ago
+ ++m_consecutive_fast_evicitons;
+ } else {
+ m_consecutive_fast_evicitons = 0;
+ }
+ }
+ m_last_cache_miss_by_eviciton = now;
+ if (m_consecutive_fast_evicitons >= m_consecutive_fast_evicitons_threshold) {
+ // double the cache size
+ auto newSize = _cacheLimit() * 2;
+ qDebug() << m_consecutive_fast_evicitons << "pixmap cache misses by eviction happened too fast, doubling cache size to"
+ << newSize;
+ _setCacheLimit(newSize);
+ m_consecutive_fast_evicitons = 0;
+ return true;
+ }
+ return false;
+ }
+
+ bool _setFastEvictionThreshold(int threshold)
+ {
+ m_consecutive_fast_evicitons_threshold = threshold;
+ return true;
+ }
+
private:
static PixmapCache* s_instance;
+ QTime m_last_cache_miss_by_eviciton;
+ int m_consecutive_fast_evicitons = 0;
+ int m_consecutive_fast_evicitons_threshold = 15;
};
diff --git a/launcher/Markdown.cpp b/launcher/Markdown.cpp
new file mode 100644
index 00000000..426067bf
--- /dev/null
+++ b/launcher/Markdown.cpp
@@ -0,0 +1,31 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2023 Joshua Goins <josh@redstrate.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "Markdown.h"
+
+QString markdownToHTML(const QString& markdown)
+{
+ const QByteArray markdownData = markdown.toUtf8();
+ char* buffer = cmark_markdown_to_html(markdownData.constData(), markdownData.length(), CMARK_OPT_NOBREAKS | CMARK_OPT_UNSAFE);
+
+ QString htmlStr(buffer);
+
+ free(buffer);
+
+ return htmlStr;
+} \ No newline at end of file
diff --git a/launcher/Markdown.h b/launcher/Markdown.h
index f115dd57..6b261e60 100644
--- a/launcher/Markdown.h
+++ b/launcher/Markdown.h
@@ -21,14 +21,4 @@
#include <QString>
#include <cmark.h>
-static QString markdownToHTML(const QString& markdown)
-{
- const QByteArray markdownData = markdown.toUtf8();
- char* buffer = cmark_markdown_to_html(markdownData.constData(), markdownData.length(), CMARK_OPT_NOBREAKS | CMARK_OPT_UNSAFE);
-
- QString htmlStr(buffer);
-
- free(buffer);
-
- return htmlStr;
-} \ No newline at end of file
+QString markdownToHTML(const QString& markdown); \ No newline at end of file
diff --git a/launcher/QVariantUtils.h b/launcher/QVariantUtils.h
new file mode 100644
index 00000000..7e422c3e
--- /dev/null
+++ b/launcher/QVariantUtils.h
@@ -0,0 +1,70 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ * Copyright (C) 2023 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/>.
+ *
+ * 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 <QVariant>
+#include <QList>
+
+namespace QVariantUtils {
+
+template <typename T>
+inline QList<T> toList(QVariant src) {
+ QVariantList variantList = src.toList();
+
+ QList<T> list_t;
+ list_t.reserve(variantList.size());
+ for (const QVariant& v : variantList)
+ {
+ list_t.append(v.value<T>());
+ }
+ return list_t;
+}
+
+template <typename T>
+inline QVariant fromList(QList<T> val) {
+ QVariantList variantList;
+ variantList.reserve(val.size());
+ for (const T& v : val)
+ {
+ variantList.append(v);
+ }
+
+ return variantList;
+}
+
+} \ No newline at end of file
diff --git a/launcher/ResourceDownloadTask.cpp b/launcher/ResourceDownloadTask.cpp
index 98bcf259..06c03c77 100644
--- a/launcher/ResourceDownloadTask.cpp
+++ b/launcher/ResourceDownloadTask.cpp
@@ -1,21 +1,21 @@
// 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/>.
-*/
+/*
+ * 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"
@@ -24,14 +24,15 @@
#include "minecraft/mod/ModFolderModel.h"
#include "minecraft/mod/ResourceFolderModel.h"
-ResourceDownloadTask::ResourceDownloadTask(ModPlatform::IndexedPack pack,
+ResourceDownloadTask::ResourceDownloadTask(ModPlatform::IndexedPack::Ptr 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)
+ bool is_indexed,
+ QString custom_target_folder)
+ : m_pack(std::move(pack)), m_pack_version(std::move(version)), m_pack_model(packs), m_custom_target_folder(custom_target_folder)
{
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));
+ 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);
@@ -40,19 +41,20 @@ ResourceDownloadTask::ResourceDownloadTask(ModPlatform::IndexedPack pack,
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() };
+ 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()) {
+ if (!m_custom_target_folder.isEmpty()) {
dir.cdUp();
- dir.cd(m_pack_version.custom_target_folder);
+ dir.cd(m_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::stepProgress, this, &ResourceDownloadTask::propogateStepProgress);
connect(m_filesNetJob.get(), &NetJob::failed, this, &ResourceDownloadTask::downloadFailed);
addTask(m_filesNetJob);
diff --git a/launcher/ResourceDownloadTask.h b/launcher/ResourceDownloadTask.h
index 73ad2d07..2baddf8a 100644
--- a/launcher/ResourceDownloadTask.h
+++ b/launcher/ResourceDownloadTask.h
@@ -1,44 +1,53 @@
// 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/>.
-*/
+ * 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/>.
+ */
#pragma once
#include "net/NetJob.h"
#include "tasks/SequentialTask.h"
-#include "modplatform/ModIndex.h"
#include "minecraft/mod/tasks/LocalModUpdateTask.h"
+#include "modplatform/ModIndex.h"
class ResourceFolderModel;
class ResourceDownloadTask : public SequentialTask {
Q_OBJECT
-public:
- explicit ResourceDownloadTask(ModPlatform::IndexedPack pack, ModPlatform::IndexedVersion version, const std::shared_ptr<ResourceFolderModel> packs, bool is_indexed = true);
+ public:
+ explicit ResourceDownloadTask(ModPlatform::IndexedPack::Ptr pack,
+ ModPlatform::IndexedVersion version,
+ const std::shared_ptr<ResourceFolderModel> packs,
+ bool is_indexed = true,
+ QString custom_target_folder = {});
const QString& getFilename() const { return m_pack_version.fileName; }
- const QString& getCustomPath() const { return m_pack_version.custom_target_folder; }
+ const QString& getCustomPath() const { return m_custom_target_folder; }
const QVariant& getVersionID() const { return m_pack_version.fileId; }
+ const ModPlatform::IndexedVersion& getVersion() const { return m_pack_version; }
+ const ModPlatform::ResourceProvider& getProvider() const { return m_pack->provider; }
+ const QString& getName() const { return m_pack->name; }
+ ModPlatform::IndexedPack::Ptr getPack() { return m_pack; }
-private:
- ModPlatform::IndexedPack m_pack;
+ private:
+ ModPlatform::IndexedPack::Ptr m_pack;
ModPlatform::IndexedVersion m_pack_version;
const std::shared_ptr<ResourceFolderModel> m_pack_model;
+ QString m_custom_target_folder;
NetJob::Ptr m_filesNetJob;
LocalModUpdateTask::Ptr m_update_task;
@@ -47,11 +56,8 @@ private:
void downloadFailed(QString reason);
void downloadSucceeded();
- std::tuple<QString, QString> to_delete {"", ""};
+ std::tuple<QString, QString> to_delete{ "", "" };
-private slots:
+ private slots:
void hasOldResource(QString name, QString filename);
};
-
-
-
diff --git a/launcher/StringUtils.cpp b/launcher/StringUtils.cpp
index 0f3c3669..e08e6fdc 100644
--- a/launcher/StringUtils.cpp
+++ b/launcher/StringUtils.cpp
@@ -1,5 +1,45 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ * Copyright (C) 2023 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/>.
+ *
+ * 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 "StringUtils.h"
+#include <QRegularExpression>
+#include <QUuid>
+#include <cmath>
+
/// If you're wondering where these came from exactly, then know you're not the only one =D
/// TAKEN FROM Qt, because it doesn't expose it intelligently
@@ -74,3 +114,71 @@ int StringUtils::naturalCompare(const QString& s1, const QString& s2, Qt::CaseSe
// The two strings are the same (02 == 2) so fall back to the normal sort
return QString::compare(s1, s2, cs);
}
+
+QString StringUtils::truncateUrlHumanFriendly(QUrl& url, int max_len, bool hard_limit)
+{
+ auto display_options = QUrl::RemoveUserInfo | QUrl::RemoveFragment | QUrl::NormalizePathSegments;
+ auto str_url = url.toDisplayString(display_options);
+
+ if (str_url.length() <= max_len)
+ return str_url;
+
+ auto url_path_parts = url.path().split('/');
+ QString last_path_segment = url_path_parts.takeLast();
+
+ if (url_path_parts.size() >= 1 && url_path_parts.first().isEmpty())
+ url_path_parts.removeFirst(); // drop empty first segment (from leading / )
+
+ if (url_path_parts.size() >= 1)
+ url_path_parts.removeLast(); // drop the next to last path segment
+
+ auto url_template = QStringLiteral("%1://%2/%3%4");
+
+ auto url_compact = url_path_parts.isEmpty()
+ ? url_template.arg(url.scheme(), url.host(), QStringList({ "...", last_path_segment }).join('/'), url.query())
+ : url_template.arg(url.scheme(), url.host(),
+ QStringList({ url_path_parts.join('/'), "...", last_path_segment }).join('/'), url.query());
+
+ // remove url parts one by one if it's still too long
+ while (url_compact.length() > max_len && url_path_parts.size() >= 1) {
+ url_path_parts.removeLast(); // drop the next to last path segment
+ url_compact = url_path_parts.isEmpty()
+ ? url_template.arg(url.scheme(), url.host(), QStringList({ "...", last_path_segment }).join('/'), url.query())
+ : url_template.arg(url.scheme(), url.host(),
+ QStringList({ url_path_parts.join('/'), "...", last_path_segment }).join('/'), url.query());
+ }
+
+ if ((url_compact.length() >= max_len) && hard_limit) {
+ // still too long, truncate normaly
+ url_compact = QString(str_url);
+ auto to_remove = url_compact.length() - max_len + 3;
+ url_compact.remove(url_compact.length() - to_remove - 1, to_remove);
+ url_compact.append("...");
+ }
+
+ return url_compact;
+}
+
+static const QStringList s_units_si{ "KB", "MB", "GB", "TB" };
+static const QStringList s_units_kibi{ "KiB", "MiB", "GiB", "TiB" };
+
+QString StringUtils::humanReadableFileSize(double bytes, bool use_si, int decimal_points)
+{
+ const QStringList units = use_si ? s_units_si : s_units_kibi;
+ const int scale = use_si ? 1000 : 1024;
+
+ int u = -1;
+ double r = pow(10, decimal_points);
+
+ do {
+ bytes /= scale;
+ u++;
+ } while (round(abs(bytes) * r) / r >= scale && u < units.length() - 1);
+
+ return QString::number(bytes, 'f', 2) + " " + units[u];
+}
+
+QString StringUtils::getRandomAlphaNumeric()
+{
+ return QUuid::createUuid().toString(QUuid::Id128);
+}
diff --git a/launcher/StringUtils.h b/launcher/StringUtils.h
index 1799605b..f90a6ac7 100644
--- a/launcher/StringUtils.h
+++ b/launcher/StringUtils.h
@@ -1,6 +1,43 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ * Copyright (C) 2023 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/>.
+ *
+ * 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 <QUrl>
namespace StringUtils {
@@ -29,4 +66,17 @@ inline QString fromStdString(string s)
#endif
int naturalCompare(const QString& s1, const QString& s2, Qt::CaseSensitivity cs);
+
+/**
+ * @brief Truncate a url while keeping its readability py placing the `...` in the middle of the path
+ * @param url Url to truncate
+ * @param max_len max lenght of url in charaters
+ * @param hard_limit if truncating the path can't get the url short enough, truncate it normaly.
+ */
+QString truncateUrlHumanFriendly(QUrl &url, int max_len, bool hard_limit = false);
+
+QString humanReadableFileSize(double bytes, bool use_si = false, int decimal_points = 1);
+
+
+QString getRandomAlphaNumeric();
} // namespace StringUtils
diff --git a/launcher/VersionProxyModel.cpp b/launcher/VersionProxyModel.cpp
index 6aba268d..63a43465 100644
--- a/launcher/VersionProxyModel.cpp
+++ b/launcher/VersionProxyModel.cpp
@@ -1,7 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
- * PolyMC - Minecraft Launcher
+ * Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ * Copyright (C) 2023 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
@@ -54,9 +55,14 @@ public:
bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
{
const auto &filters = m_parent->filters();
+ const QString &search = m_parent->search();
+ const QModelIndex idx = sourceModel()->index(source_row, 0, source_parent);
+
+ if (!search.isEmpty() && !sourceModel()->data(idx, BaseVersionList::VersionRole).toString().contains(search, Qt::CaseInsensitive))
+ return false;
+
for (auto it = filters.begin(); it != filters.end(); ++it)
{
- auto idx = sourceModel()->index(source_row, 0, source_parent);
auto data = sourceModel()->data(idx, it.key());
auto match = data.toString();
if(!it.value()->accepts(match))
@@ -187,35 +193,21 @@ QVariant VersionProxyModel::data(const QModelIndex &index, int role) const
}
case Qt::ToolTipRole:
{
- switch(column)
+ if(column == Name && hasRecommended)
{
- case Name:
+ auto value = sourceModel()->data(parentIndex, BaseVersionList::RecommendedRole);
+ if(value.toBool())
{
- if(hasRecommended)
+ return tr("Recommended");
+ } else if(hasLatest) {
+ auto value = sourceModel()->data(parentIndex, BaseVersionList::LatestRole);
+ if(value.toBool())
{
- auto value = sourceModel()->data(parentIndex, BaseVersionList::RecommendedRole);
- if(value.toBool())
- {
- return tr("Recommended");
- }
- else if(hasLatest)
- {
- auto value = sourceModel()->data(parentIndex, BaseVersionList::LatestRole);
- if(value.toBool())
- {
- return tr("Latest");
- }
- }
- else if(index.row() == 0)
- {
- return tr("Latest");
- }
+ return tr("Latest");
}
}
- default:
- {
- return sourceModel()->data(parentIndex, BaseVersionList::VersionIdRole);
- }
+ } else {
+ return sourceModel()->data(parentIndex, BaseVersionList::VersionIdRole);
}
}
case Qt::DecorationRole:
@@ -239,10 +231,6 @@ QVariant VersionProxyModel::data(const QModelIndex &index, int role) const
return APPLICATION->getThemedIcon("bug");
}
}
- else if(index.row() == 0)
- {
- return APPLICATION->getThemedIcon("bug");
- }
QPixmap pixmap;
QPixmapCache::find("placeholder", &pixmap);
if(!pixmap)
@@ -431,6 +419,7 @@ QModelIndex VersionProxyModel::getVersion(const QString& version) const
void VersionProxyModel::clearFilters()
{
m_filters.clear();
+ m_search.clear();
filterModel->filterChanged();
}
@@ -440,11 +429,21 @@ void VersionProxyModel::setFilter(const BaseVersionList::ModelRoles column, Filt
filterModel->filterChanged();
}
+void VersionProxyModel::setSearch(const QString &search) {
+ m_search = search;
+ filterModel->filterChanged();
+}
+
const VersionProxyModel::FilterMap &VersionProxyModel::filters() const
{
return m_filters;
}
+const QString &VersionProxyModel::search() const
+{
+ return m_search;
+}
+
void VersionProxyModel::sourceAboutToBeReset()
{
beginResetModel();
diff --git a/launcher/VersionProxyModel.h b/launcher/VersionProxyModel.h
index 8991c31b..6434376c 100644
--- a/launcher/VersionProxyModel.h
+++ b/launcher/VersionProxyModel.h
@@ -38,7 +38,9 @@ public:
virtual void setSourceModel(QAbstractItemModel *sourceModel) override;
const FilterMap &filters() const;
+ const QString &search() const;
void setFilter(const BaseVersionList::ModelRoles column, Filter * filter);
+ void setSearch(const QString &search);
void clearFilters();
QModelIndex getRecommended() const;
QModelIndex getVersion(const QString & version) const;
@@ -59,6 +61,7 @@ private slots:
private:
QList<Column> m_columns;
FilterMap m_filters;
+ QString m_search;
BaseVersionList::RoleList roles;
VersionFilterModel * filterModel;
bool hasRecommended = false;
diff --git a/launcher/filelink/FileLink.cpp b/launcher/filelink/FileLink.cpp
new file mode 100644
index 00000000..c9599b82
--- /dev/null
+++ b/launcher/filelink/FileLink.cpp
@@ -0,0 +1,277 @@
+// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "FileLink.h"
+#include "BuildConfig.h"
+
+#include "StringUtils.h"
+
+#include <iostream>
+
+#include <QAccessible>
+#include <QCommandLineParser>
+
+#include <QDebug>
+
+#include <DesktopServices.h>
+
+#include <sys.h>
+
+#if defined Q_OS_WIN32
+#ifndef WIN32_LEAN_AND_MEAN
+#define WIN32_LEAN_AND_MEAN
+#endif
+#include <stdio.h>
+#include <windows.h>
+#endif
+
+// Snippet from https://github.com/gulrak/filesystem#using-it-as-single-file-header
+
+#ifdef __APPLE__
+#include <Availability.h> // for deployment target to support pre-catalina targets without std::fs
+#endif // __APPLE__
+
+#if ((defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) || (defined(__cplusplus) && __cplusplus >= 201703L)) && defined(__has_include)
+#if __has_include(<filesystem>) && (!defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || __MAC_OS_X_VERSION_MIN_REQUIRED >= 101500)
+#define GHC_USE_STD_FS
+#include <filesystem>
+namespace fs = std::filesystem;
+#endif // MacOS min version check
+#endif // Other OSes version check
+
+#ifndef GHC_USE_STD_FS
+#include <ghc/filesystem.hpp>
+namespace fs = ghc::filesystem;
+#endif
+
+FileLinkApp::FileLinkApp(int& argc, char** argv) : QCoreApplication(argc, argv), socket(new QLocalSocket(this))
+{
+#if defined Q_OS_WIN32
+ // attach the parent console
+ if (AttachConsole(ATTACH_PARENT_PROCESS)) {
+ // if attach succeeds, reopen and sync all the i/o
+ if (freopen("CON", "w", stdout)) {
+ std::cout.sync_with_stdio();
+ }
+ if (freopen("CON", "w", stderr)) {
+ std::cerr.sync_with_stdio();
+ }
+ if (freopen("CON", "r", stdin)) {
+ std::cin.sync_with_stdio();
+ }
+ auto out = GetStdHandle(STD_OUTPUT_HANDLE);
+ DWORD written;
+ const char* endline = "\n";
+ WriteConsole(out, endline, strlen(endline), &written, NULL);
+ consoleAttached = true;
+ }
+#endif
+ setOrganizationName(BuildConfig.LAUNCHER_NAME);
+ setOrganizationDomain(BuildConfig.LAUNCHER_DOMAIN);
+ setApplicationName(BuildConfig.LAUNCHER_NAME + "FileLink");
+ setApplicationVersion(BuildConfig.printableVersionString() + "\n" + BuildConfig.GIT_COMMIT);
+
+ // Commandline parsing
+ QCommandLineParser parser;
+ parser.setApplicationDescription(QObject::tr("a batch MKLINK program for windows to be used with prismlauncher"));
+
+ parser.addOptions({ { { "s", "server" }, "Join the specified server on launch", "pipe name" },
+ { { "H", "hard" }, "use hard links instead of symbolic", "true/false" } });
+ parser.addHelpOption();
+ parser.addVersionOption();
+
+ parser.process(arguments());
+
+ QString serverToJoin = parser.value("server");
+ m_useHardLinks = QVariant(parser.value("hard")).toBool();
+
+ qDebug() << "link program launched";
+
+ if (!serverToJoin.isEmpty()) {
+ qDebug() << "joining server" << serverToJoin;
+ joinServer(serverToJoin);
+ } else {
+ qDebug() << "no server to join";
+ exit();
+ }
+}
+
+void FileLinkApp::joinServer(QString server)
+{
+ blockSize = 0;
+
+ in.setDevice(&socket);
+
+ connect(&socket, &QLocalSocket::connected, this, [&]() { qDebug() << "connected to server"; });
+
+ connect(&socket, &QLocalSocket::readyRead, this, &FileLinkApp::readPathPairs);
+
+ connect(&socket, &QLocalSocket::errorOccurred, this, [&](QLocalSocket::LocalSocketError socketError) {
+ switch (socketError) {
+ case QLocalSocket::ServerNotFoundError:
+ qDebug()
+ << ("The host was not found. Please make sure "
+ "that the server is running and that the "
+ "server name is correct.");
+ break;
+ case QLocalSocket::ConnectionRefusedError:
+ qDebug()
+ << ("The connection was refused by the peer. "
+ "Make sure the server is running, "
+ "and check that the server name "
+ "is correct.");
+ break;
+ case QLocalSocket::PeerClosedError:
+ qDebug() << ("The connection was closed by the peer. ");
+ break;
+ default:
+ qDebug() << "The following error occurred: " << socket.errorString();
+ }
+ });
+
+ connect(&socket, &QLocalSocket::disconnected, this, [&]() {
+ qDebug() << "disconnected from server, should exit";
+ exit();
+ });
+
+ socket.connectToServer(server);
+}
+
+void FileLinkApp::runLink()
+{
+ std::error_code os_err;
+
+ qDebug() << "creating links";
+
+ for (auto link : m_links_to_make) {
+ QString src_path = link.src;
+ QString dst_path = link.dst;
+
+ FS::ensureFilePathExists(dst_path);
+ if (m_useHardLinks) {
+ qDebug() << "making hard link:" << src_path << "to" << dst_path;
+ fs::create_hard_link(StringUtils::toStdString(src_path), StringUtils::toStdString(dst_path), os_err);
+ } else if (fs::is_directory(StringUtils::toStdString(src_path))) {
+ qDebug() << "making directory_symlink:" << src_path << "to" << dst_path;
+ fs::create_directory_symlink(StringUtils::toStdString(src_path), StringUtils::toStdString(dst_path), os_err);
+ } else {
+ qDebug() << "making symlink:" << src_path << "to" << dst_path;
+ fs::create_symlink(StringUtils::toStdString(src_path), StringUtils::toStdString(dst_path), os_err);
+ }
+
+ if (os_err) {
+ qWarning() << "Failed to link files:" << QString::fromStdString(os_err.message());
+ qDebug() << "Source file:" << src_path;
+ qDebug() << "Destination file:" << dst_path;
+ qDebug() << "Error category:" << os_err.category().name();
+ qDebug() << "Error code:" << os_err.value();
+
+ FS::LinkResult result = { src_path, dst_path, QString::fromStdString(os_err.message()), os_err.value() };
+ m_path_results.append(result);
+ } else {
+ FS::LinkResult result = { src_path, dst_path };
+ m_path_results.append(result);
+ }
+ }
+
+ sendResults();
+ qDebug() << "done, should exit soon";
+}
+
+void FileLinkApp::sendResults()
+{
+ // construct block of data to send
+ QByteArray block;
+ QDataStream out(&block, QIODevice::WriteOnly);
+
+ qint32 blocksize = quint32(sizeof(quint32));
+ for (auto result : m_path_results) {
+ blocksize += quint32(result.src.size());
+ blocksize += quint32(result.dst.size());
+ blocksize += quint32(result.err_msg.size());
+ blocksize += quint32(sizeof(quint32));
+ }
+ qDebug() << "About to write block of size:" << blocksize;
+ out << blocksize;
+
+ out << quint32(m_path_results.length());
+ for (auto result : m_path_results) {
+ out << result.src;
+ out << result.dst;
+ out << result.err_msg;
+ out << quint32(result.err_value);
+ }
+
+ qint64 byteswritten = socket.write(block);
+ bool bytesflushed = socket.flush();
+ qDebug() << "block flushed" << byteswritten << bytesflushed;
+}
+
+void FileLinkApp::readPathPairs()
+{
+ m_links_to_make.clear();
+ qDebug() << "Reading path pairs from server";
+ qDebug() << "bytes available" << socket.bytesAvailable();
+ if (blockSize == 0) {
+ // Relies on the fact that QDataStream serializes a quint32 into
+ // sizeof(quint32) bytes
+ if (socket.bytesAvailable() < (int)sizeof(quint32))
+ return;
+ qDebug() << "reading block size";
+ in >> blockSize;
+ }
+ qDebug() << "blocksize is" << blockSize;
+ qDebug() << "bytes available" << socket.bytesAvailable();
+ if (socket.bytesAvailable() < blockSize || in.atEnd())
+ return;
+
+ quint32 numLinks;
+ in >> numLinks;
+ qDebug() << "numLinks" << numLinks;
+
+ for (int i = 0; i < numLinks; i++) {
+ FS::LinkPair pair;
+ in >> pair.src;
+ in >> pair.dst;
+ qDebug() << "link" << pair.src << "to" << pair.dst;
+ m_links_to_make.append(pair);
+ }
+
+ runLink();
+}
+
+FileLinkApp::~FileLinkApp()
+{
+ qDebug() << "link program shutting down";
+ // Shut down logger by setting the logger function to nothing
+ qInstallMessageHandler(nullptr);
+
+#if defined Q_OS_WIN32
+ // Detach from Windows console
+ if (consoleAttached) {
+ fclose(stdout);
+ fclose(stdin);
+ fclose(stderr);
+ FreeConsole();
+ }
+#endif
+}
diff --git a/launcher/filelink/FileLink.h b/launcher/filelink/FileLink.h
new file mode 100644
index 00000000..4c47d9bb
--- /dev/null
+++ b/launcher/filelink/FileLink.h
@@ -0,0 +1,67 @@
+// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.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 <QtCore>
+
+#include <QApplication>
+#include <QDataStream>
+#include <QDateTime>
+#include <QDebug>
+#include <QFlag>
+#include <QIcon>
+#include <QLocalSocket>
+#include <QUrl>
+#include <memory>
+
+#define PRISM_EXTERNAL_EXE
+#include "FileSystem.h"
+
+class FileLinkApp : public QCoreApplication {
+ // friends for the purpose of limiting access to deprecated stuff
+ Q_OBJECT
+ public:
+ FileLinkApp(int& argc, char** argv);
+ virtual ~FileLinkApp();
+
+ private:
+ void joinServer(QString server);
+ void readPathPairs();
+ void runLink();
+ void sendResults();
+
+ bool m_useHardLinks = false;
+
+ QDateTime m_startTime;
+ QLocalSocket socket;
+ QDataStream in;
+ quint32 blockSize;
+
+ QList<FS::LinkPair> m_links_to_make;
+ QList<FS::LinkResult> m_path_results;
+
+#if defined Q_OS_WIN32
+ // used on Windows to attach the standard IO streams
+ bool consoleAttached = false;
+#endif
+};
diff --git a/launcher/filelink/filelink.exe.manifest b/launcher/filelink/filelink.exe.manifest
new file mode 100644
index 00000000..239aa978
--- /dev/null
+++ b/launcher/filelink/filelink.exe.manifest
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
+ <application xmlns="urn:schemas-microsoft-com:asm.v3">
+ <windowsSettings>
+ </windowsSettings>
+ </application>
+ <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
+ <application>
+ <!-- Windows 10, Windows 11 -->
+ <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
+ <!-- Windows 8.1 -->
+ <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
+ <!-- Windows 8 -->
+ <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
+ <!-- Windows 7 -->
+ <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
+ </application>
+ </compatibility>
+ <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
+ <security>
+ <requestedPrivileges>
+ <requestedExecutionLevel
+ level="requireAdministrator"
+ uiAccess="false"/>
+ </requestedPrivileges>
+ </security>
+ </trustInfo>
+</assembly>
diff --git a/launcher/filelink/main.cpp b/launcher/filelink/main.cpp
new file mode 100644
index 00000000..83566a3c
--- /dev/null
+++ b/launcher/filelink/main.cpp
@@ -0,0 +1,30 @@
+// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "FileLink.h"
+
+int main(int argc, char* argv[])
+{
+ FileLinkApp ldh(argc, argv);
+
+ return ldh.exec();
+}
diff --git a/launcher/icons/IconList.cpp b/launcher/icons/IconList.cpp
index 1dfc6432..13174f6e 100644
--- a/launcher/icons/IconList.cpp
+++ b/launcher/icons/IconList.cpp
@@ -66,9 +66,8 @@ IconList::IconList(const QStringList &builtinPaths, QString path, QObject *paren
m_watcher.reset(new QFileSystemWatcher());
is_watching = false;
- connect(m_watcher.get(), SIGNAL(directoryChanged(QString)),
- SLOT(directoryChanged(QString)));
- connect(m_watcher.get(), SIGNAL(fileChanged(QString)), SLOT(fileChanged(QString)));
+ connect(m_watcher.get(), &QFileSystemWatcher::directoryChanged, this, &IconList::directoryChanged);
+ connect(m_watcher.get(), &QFileSystemWatcher::fileChanged, this, &IconList::fileChanged);
directoryChanged(path);
diff --git a/launcher/java/JavaChecker.cpp b/launcher/java/JavaChecker.cpp
index 041583d1..e4a686c2 100644
--- a/launcher/java/JavaChecker.cpp
+++ b/launcher/java/JavaChecker.cpp
@@ -85,17 +85,13 @@ void JavaChecker::performCheck()
process->setProgram(m_path);
process->setProcessChannelMode(QProcess::SeparateChannels);
process->setProcessEnvironment(CleanEnviroment());
- qDebug() << "Running java checker: " + m_path + args.join(" ");;
+ qDebug() << "Running java checker:" << m_path << args.join(" ");
- connect(process.get(), SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(finished(int, QProcess::ExitStatus)));
-#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
- connect(process.get(), SIGNAL(errorOccurred(QProcess::ProcessError)), this, SLOT(error(QProcess::ProcessError)));
-#else
- connect(process.get(), SIGNAL(error(QProcess::ProcessError)), this, SLOT(error(QProcess::ProcessError)));
-#endif
- connect(process.get(), SIGNAL(readyReadStandardOutput()), this, SLOT(stdoutReady()));
- connect(process.get(), SIGNAL(readyReadStandardError()), this, SLOT(stderrReady()));
- connect(&killTimer, SIGNAL(timeout()), SLOT(timeout()));
+ connect(process.get(), QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this, &JavaChecker::finished);
+ connect(process.get(), &QProcess::errorOccurred, this, &JavaChecker::error);
+ connect(process.get(), &QProcess::readyReadStandardOutput, this, &JavaChecker::stdoutReady);
+ connect(process.get(), &QProcess::readyReadStandardError, this, &JavaChecker::stderrReady);
+ connect(&killTimer, &QTimer::timeout, this, &JavaChecker::timeout);
killTimer.setSingleShot(true);
killTimer.start(15000);
process->start();
@@ -132,7 +128,7 @@ void JavaChecker::finished(int exitcode, QProcess::ExitStatus status)
result.outLog = m_stdout;
qDebug() << "STDOUT" << m_stdout;
qWarning() << "STDERR" << m_stderr;
- qDebug() << "Java checker finished with status " << status << " exit code " << exitcode;
+ qDebug() << "Java checker finished with status" << status << "exit code" << exitcode;
if (status == QProcess::CrashExit || exitcode == 1)
{
diff --git a/launcher/java/JavaCheckerJob.cpp b/launcher/java/JavaCheckerJob.cpp
index 67d70066..48274974 100644
--- a/launcher/java/JavaCheckerJob.cpp
+++ b/launcher/java/JavaCheckerJob.cpp
@@ -38,7 +38,7 @@ void JavaCheckerJob::executeTask()
for (auto iter : javacheckers)
{
javaresults.append(JavaCheckResult());
- connect(iter.get(), SIGNAL(checkFinished(JavaCheckResult)), SLOT(partFinished(JavaCheckResult)));
+ connect(iter.get(), &JavaChecker::checkFinished, this, &JavaCheckerJob::partFinished);
iter->performCheck();
}
}
diff --git a/launcher/java/JavaInstallList.cpp b/launcher/java/JavaInstallList.cpp
index b29af857..3407fdf7 100644
--- a/launcher/java/JavaInstallList.cpp
+++ b/launcher/java/JavaInstallList.cpp
@@ -1,7 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
- * PolyMC - Minecraft Launcher
+ * Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ * Copyright (C) 2023 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
@@ -98,6 +99,8 @@ QVariant JavaInstallList::data(const QModelIndex &index, int role) const
auto version = std::dynamic_pointer_cast<JavaInstall>(m_vlist[index.row()]);
switch (role)
{
+ case SortRole:
+ return -index.row();
case VersionPointerRole:
return QVariant::fromValue(m_vlist[index.row()]);
case VersionIdRole:
diff --git a/launcher/launch/steps/CheckJava.cpp b/launcher/launch/steps/CheckJava.cpp
index f0187586..7d697ba9 100644
--- a/launcher/launch/steps/CheckJava.cpp
+++ b/launcher/launch/steps/CheckJava.cpp
@@ -81,15 +81,20 @@ void CheckJava::executeTask()
}
QFileInfo javaInfo(realJavaPath);
- qlonglong javaUnixTime = javaInfo.lastModified().toMSecsSinceEpoch();
- auto storedUnixTime = settings->get("JavaTimestamp").toLongLong();
+ qint64 javaUnixTime = javaInfo.lastModified().toMSecsSinceEpoch();
+ auto storedSignature = settings->get("JavaSignature").toString();
auto storedArchitecture = settings->get("JavaArchitecture").toString();
auto storedRealArchitecture = settings->get("JavaRealArchitecture").toString();
auto storedVersion = settings->get("JavaVersion").toString();
auto storedVendor = settings->get("JavaVendor").toString();
- m_javaUnixTime = javaUnixTime;
+
+ QCryptographicHash hash(QCryptographicHash::Sha1);
+ hash.addData(QByteArray::number(javaUnixTime));
+ hash.addData(m_javaPath.toUtf8());
+ m_javaSignature = hash.result().toHex();
+
// if timestamps are not the same, or something is missing, check!
- if (javaUnixTime != storedUnixTime || storedVersion.size() == 0
+ if (m_javaSignature != storedSignature || storedVersion.size() == 0
|| storedArchitecture.size() == 0 || storedRealArchitecture.size() == 0
|| storedVendor.size() == 0)
{
@@ -140,7 +145,7 @@ void CheckJava::checkJavaFinished(JavaCheckResult result)
instance->settings()->set("JavaArchitecture", result.mojangPlatform);
instance->settings()->set("JavaRealArchitecture", result.realPlatform);
instance->settings()->set("JavaVendor", result.javaVendor);
- instance->settings()->set("JavaTimestamp", m_javaUnixTime);
+ instance->settings()->set("JavaSignature", m_javaSignature);
emitSucceeded();
return;
}
diff --git a/launcher/launch/steps/CheckJava.h b/launcher/launch/steps/CheckJava.h
index d084b132..bbf06b7c 100644
--- a/launcher/launch/steps/CheckJava.h
+++ b/launcher/launch/steps/CheckJava.h
@@ -40,6 +40,6 @@ private:
private:
QString m_javaPath;
- qlonglong m_javaUnixTime;
+ QString m_javaSignature;
JavaCheckerPtr m_JavaChecker;
};
diff --git a/launcher/launch/steps/Update.cpp b/launcher/launch/steps/Update.cpp
index 28bd153d..77c8a18e 100644
--- a/launcher/launch/steps/Update.cpp
+++ b/launcher/launch/steps/Update.cpp
@@ -26,9 +26,11 @@ void Update::executeTask()
m_updateTask.reset(m_parent->instance()->createUpdateTask(m_mode));
if(m_updateTask)
{
- connect(m_updateTask.get(), SIGNAL(finished()), this, SLOT(updateFinished()));
- connect(m_updateTask.get(), &Task::progress, this, &Task::setProgress);
- connect(m_updateTask.get(), &Task::status, this, &Task::setStatus);
+ connect(m_updateTask.get(), &Task::finished, this, &Update::updateFinished);
+ connect(m_updateTask.get(), &Task::progress, this, &Update::setProgress);
+ connect(m_updateTask.get(), &Task::stepProgress, this, &Update::propogateStepProgress);
+ connect(m_updateTask.get(), &Task::status, this, &Update::setStatus);
+ connect(m_updateTask.get(), &Task::details, this, &Update::setDetails);
emit progressReportingRequest();
return;
}
diff --git a/launcher/meta/Index.cpp b/launcher/meta/Index.cpp
index 242aad9f..4dccccca 100644
--- a/launcher/meta/Index.cpp
+++ b/launcher/meta/Index.cpp
@@ -45,10 +45,10 @@ QVariant Index::data(const QModelIndex &index, int role) const
switch (role)
{
case Qt::DisplayRole:
- switch (index.column())
- {
- case 0: return list->humanReadable();
- default: break;
+ if (index.column() == 0) {
+ return list->humanReadable();
+ } else {
+ break;
}
case UidRole: return list->uid();
case NameRole: return list->name();
diff --git a/launcher/meta/JsonFormat.cpp b/launcher/meta/JsonFormat.cpp
index 473f37d6..cb2d06ea 100644
--- a/launcher/meta/JsonFormat.cpp
+++ b/launcher/meta/JsonFormat.cpp
@@ -56,10 +56,10 @@ static Version::Ptr parseCommonVersion(const QString &uid, const QJsonObject &ob
version->setType(ensureString(obj, "type", QString()));
version->setRecommended(ensureBoolean(obj, QString("recommended"), false));
version->setVolatile(ensureBoolean(obj, QString("volatile"), false));
- RequireSet requires, conflicts;
- parseRequires(obj, &requires, "requires");
+ RequireSet reqs, conflicts;
+ parseRequires(obj, &reqs, "requires");
parseRequires(obj, &conflicts, "conflicts");
- version->setRequires(requires, conflicts);
+ version->setRequires(reqs, conflicts);
return version;
}
@@ -176,7 +176,6 @@ void parseRequires(const QJsonObject& obj, RequireSet* ptr, const char * keyName
{
if(obj.contains(keyName))
{
- QSet<QString> requires;
auto reqArray = requireArray(obj, keyName);
auto iter = reqArray.begin();
while(iter != reqArray.end())
diff --git a/launcher/meta/Version.cpp b/launcher/meta/Version.cpp
index e617abf8..0718a420 100644
--- a/launcher/meta/Version.cpp
+++ b/launcher/meta/Version.cpp
@@ -116,9 +116,9 @@ void Meta::Version::setTime(const qint64 time)
emit timeChanged();
}
-void Meta::Version::setRequires(const Meta::RequireSet &requires, const Meta::RequireSet &conflicts)
+void Meta::Version::setRequires(const Meta::RequireSet &reqs, const Meta::RequireSet &conflicts)
{
- m_requires = requires;
+ m_requires = reqs;
m_conflicts = conflicts;
emit requiresChanged();
}
diff --git a/launcher/meta/Version.h b/launcher/meta/Version.h
index 78156193..59a96a68 100644
--- a/launcher/meta/Version.h
+++ b/launcher/meta/Version.h
@@ -63,7 +63,7 @@ public:
{
return m_time;
}
- const Meta::RequireSet &requires() const
+ const Meta::RequireSet &requiredSet() const
{
return m_requires;
}
@@ -91,7 +91,7 @@ public:
public: // for usage by format parsers only
void setType(const QString &type);
void setTime(const qint64 time);
- void setRequires(const Meta::RequireSet &requires, const Meta::RequireSet &conflicts);
+ void setRequires(const Meta::RequireSet &reqs, const Meta::RequireSet &conflicts);
void setVolatile(bool volatile_);
void setRecommended(bool recommended);
void setProvidesRecommendations();
diff --git a/launcher/meta/VersionList.cpp b/launcher/meta/VersionList.cpp
index 7f001dfc..9f448278 100644
--- a/launcher/meta/VersionList.cpp
+++ b/launcher/meta/VersionList.cpp
@@ -77,7 +77,7 @@ QVariant VersionList::data(const QModelIndex &index, int role) const
case ParentVersionRole:
{
// FIXME: HACK: this should be generic and be replaced by something else. Anything that is a hard 'equals' dep is a 'parent uid'.
- auto & reqs = version->requires();
+ auto & reqs = version->requiredSet();
auto iter = std::find_if(reqs.begin(), reqs.end(), [](const Require & req)
{
return req.uid == "net.minecraft";
@@ -92,7 +92,7 @@ QVariant VersionList::data(const QModelIndex &index, int role) const
case UidRole: return version->uid();
case TimeRole: return version->time();
- case RequiresRole: return QVariant::fromValue(version->requires());
+ case RequiresRole: return QVariant::fromValue(version->requiredSet());
case SortRole: return version->rawTime();
case VersionPtrRole: return QVariant::fromValue(version);
case RecommendedRole: return version->isRecommended();
diff --git a/launcher/minecraft/Component.cpp b/launcher/minecraft/Component.cpp
index 7e5b6058..ff81fcbb 100644
--- a/launcher/minecraft/Component.cpp
+++ b/launcher/minecraft/Component.cpp
@@ -451,9 +451,9 @@ void Component::updateCachedData()
m_cachedVolatile = file->m_volatile;
changed = true;
}
- if(!deepCompare(m_cachedRequires, file->requires))
+ if(!deepCompare(m_cachedRequires, file->m_requires))
{
- m_cachedRequires = file->requires;
+ m_cachedRequires = file->m_requires;
changed = true;
}
if(!deepCompare(m_cachedConflicts, file->conflicts))
diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp
index af4da5d0..4867cc7a 100644
--- a/launcher/minecraft/MinecraftInstance.cpp
+++ b/launcher/minecraft/MinecraftInstance.cpp
@@ -148,10 +148,11 @@ void MinecraftInstance::loadSpecificSettings()
m_settings->registerOverride(global_settings->getSetting("IgnoreJavaCompatibility"), javaOrLocation);
// special!
- m_settings->registerPassthrough(global_settings->getSetting("JavaTimestamp"), javaOrLocation);
- m_settings->registerPassthrough(global_settings->getSetting("JavaVersion"), javaOrLocation);
+ m_settings->registerPassthrough(global_settings->getSetting("JavaSignature"), javaOrLocation);
m_settings->registerPassthrough(global_settings->getSetting("JavaArchitecture"), javaOrLocation);
m_settings->registerPassthrough(global_settings->getSetting("JavaRealArchitecture"), javaOrLocation);
+ m_settings->registerPassthrough(global_settings->getSetting("JavaVersion"), javaOrLocation);
+ m_settings->registerPassthrough(global_settings->getSetting("JavaVendor"), javaOrLocation);
// Window Size
auto windowSetting = m_settings->registerSetting("OverrideWindow", false);
@@ -925,7 +926,10 @@ QString MinecraftInstance::getStatusbarDescription()
if(m_settings->get("ShowGameTime").toBool())
{
if (lastTimePlayed() > 0) {
- description.append(tr(", last played for %1").arg(Time::prettifyDuration(lastTimePlayed())));
+ QDateTime lastLaunchTime = QDateTime::fromMSecsSinceEpoch(lastLaunch());
+ description.append(tr(", last played on %1 for %2")
+ .arg(QLocale().toString(lastLaunchTime, QLocale::ShortFormat))
+ .arg(Time::prettifyDuration(lastTimePlayed())));
}
if (totalTimePlayed() > 0) {
@@ -1106,79 +1110,70 @@ JavaVersion MinecraftInstance::getJavaVersion()
return JavaVersion(settings()->get("JavaVersion").toString());
}
-std::shared_ptr<ModFolderModel> MinecraftInstance::loaderModList() const
+std::shared_ptr<ModFolderModel> MinecraftInstance::loaderModList()
{
- if (!m_loader_mod_list)
- {
+ if (!m_loader_mod_list) {
bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool();
- m_loader_mod_list.reset(new ModFolderModel(modsRoot(), is_indexed));
- m_loader_mod_list->disableInteraction(isRunning());
- connect(this, &BaseInstance::runningStatusChanged, m_loader_mod_list.get(), &ModFolderModel::disableInteraction);
+ m_loader_mod_list.reset(new ModFolderModel(modsRoot(), this, is_indexed));
}
return m_loader_mod_list;
}
-std::shared_ptr<ModFolderModel> MinecraftInstance::coreModList() const
+std::shared_ptr<ModFolderModel> MinecraftInstance::coreModList()
{
- if (!m_core_mod_list)
- {
+ if (!m_core_mod_list) {
bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool();
- m_core_mod_list.reset(new ModFolderModel(coreModsDir(), is_indexed));
- m_core_mod_list->disableInteraction(isRunning());
- connect(this, &BaseInstance::runningStatusChanged, m_core_mod_list.get(), &ModFolderModel::disableInteraction);
+ m_core_mod_list.reset(new ModFolderModel(coreModsDir(), this, is_indexed));
}
return m_core_mod_list;
}
-std::shared_ptr<ModFolderModel> MinecraftInstance::nilModList() const
+std::shared_ptr<ModFolderModel> MinecraftInstance::nilModList()
{
- if (!m_nil_mod_list)
- {
+ if (!m_nil_mod_list) {
bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool();
- m_nil_mod_list.reset(new ModFolderModel(nilModsDir(), is_indexed, false));
- m_nil_mod_list->disableInteraction(isRunning());
- connect(this, &BaseInstance::runningStatusChanged, m_nil_mod_list.get(), &ModFolderModel::disableInteraction);
+ m_nil_mod_list.reset(new ModFolderModel(nilModsDir(), this, is_indexed, false));
}
return m_nil_mod_list;
}
-std::shared_ptr<ResourcePackFolderModel> MinecraftInstance::resourcePackList() const
+std::shared_ptr<ResourcePackFolderModel> MinecraftInstance::resourcePackList()
{
if (!m_resource_pack_list)
{
- m_resource_pack_list.reset(new ResourcePackFolderModel(resourcePacksDir()));
+ m_resource_pack_list.reset(new ResourcePackFolderModel(resourcePacksDir(), this));
}
return m_resource_pack_list;
}
-std::shared_ptr<TexturePackFolderModel> MinecraftInstance::texturePackList() const
+std::shared_ptr<TexturePackFolderModel> MinecraftInstance::texturePackList()
{
if (!m_texture_pack_list)
{
- m_texture_pack_list.reset(new TexturePackFolderModel(texturePacksDir()));
+ m_texture_pack_list.reset(new TexturePackFolderModel(texturePacksDir(), this));
}
return m_texture_pack_list;
}
-std::shared_ptr<ShaderPackFolderModel> MinecraftInstance::shaderPackList() const
+std::shared_ptr<ShaderPackFolderModel> MinecraftInstance::shaderPackList()
{
if (!m_shader_pack_list)
{
- m_shader_pack_list.reset(new ShaderPackFolderModel(shaderPacksDir()));
+ m_shader_pack_list.reset(new ShaderPackFolderModel(shaderPacksDir(), this));
}
return m_shader_pack_list;
}
-std::shared_ptr<WorldList> MinecraftInstance::worldList() const
+std::shared_ptr<WorldList> MinecraftInstance::worldList()
{
if (!m_world_list)
{
- m_world_list.reset(new WorldList(worldDir()));
+ m_world_list.reset(new WorldList(worldDir(), this));
}
return m_world_list;
}
-std::shared_ptr<GameOptions> MinecraftInstance::gameOptionsModel() const
+std::shared_ptr<GameOptions> MinecraftInstance::gameOptionsModel()
{
if (!m_game_options)
{
diff --git a/launcher/minecraft/MinecraftInstance.h b/launcher/minecraft/MinecraftInstance.h
index a75fa481..068b3008 100644
--- a/launcher/minecraft/MinecraftInstance.h
+++ b/launcher/minecraft/MinecraftInstance.h
@@ -115,14 +115,14 @@ public:
std::shared_ptr<PackProfile> getPackProfile() const;
////// Mod Lists //////
- std::shared_ptr<ModFolderModel> loaderModList() const;
- std::shared_ptr<ModFolderModel> coreModList() const;
- std::shared_ptr<ModFolderModel> nilModList() const;
- std::shared_ptr<ResourcePackFolderModel> resourcePackList() const;
- std::shared_ptr<TexturePackFolderModel> texturePackList() const;
- std::shared_ptr<ShaderPackFolderModel> shaderPackList() const;
- std::shared_ptr<WorldList> worldList() const;
- std::shared_ptr<GameOptions> gameOptionsModel() const;
+ std::shared_ptr<ModFolderModel> loaderModList();
+ std::shared_ptr<ModFolderModel> coreModList();
+ std::shared_ptr<ModFolderModel> nilModList();
+ std::shared_ptr<ResourcePackFolderModel> resourcePackList();
+ std::shared_ptr<TexturePackFolderModel> texturePackList();
+ std::shared_ptr<ShaderPackFolderModel> shaderPackList();
+ std::shared_ptr<WorldList> worldList();
+ std::shared_ptr<GameOptions> gameOptionsModel();
////// Launch stuff //////
Task::Ptr createUpdateTask(Net::Mode mode) override;
diff --git a/launcher/minecraft/MinecraftLoadAndCheck.cpp b/launcher/minecraft/MinecraftLoadAndCheck.cpp
index d72bc7be..1c3f6fb7 100644
--- a/launcher/minecraft/MinecraftLoadAndCheck.cpp
+++ b/launcher/minecraft/MinecraftLoadAndCheck.cpp
@@ -22,6 +22,7 @@ void MinecraftLoadAndCheck::executeTask()
connect(m_task.get(), &Task::failed, this, &MinecraftLoadAndCheck::subtaskFailed);
connect(m_task.get(), &Task::aborted, this, [this]{ subtaskFailed(tr("Aborted")); });
connect(m_task.get(), &Task::progress, this, &MinecraftLoadAndCheck::progress);
+ connect(m_task.get(), &Task::stepProgress, this, &MinecraftLoadAndCheck::propogateStepProgress);
connect(m_task.get(), &Task::status, this, &MinecraftLoadAndCheck::setStatus);
}
diff --git a/launcher/minecraft/MinecraftUpdate.cpp b/launcher/minecraft/MinecraftUpdate.cpp
index 07ad4882..35430bb0 100644
--- a/launcher/minecraft/MinecraftUpdate.cpp
+++ b/launcher/minecraft/MinecraftUpdate.cpp
@@ -100,7 +100,9 @@ void MinecraftUpdate::next()
disconnect(task.get(), &Task::failed, this, &MinecraftUpdate::subtaskFailed);
disconnect(task.get(), &Task::aborted, this, &Task::abort);
disconnect(task.get(), &Task::progress, this, &MinecraftUpdate::progress);
+ disconnect(task.get(), &Task::stepProgress, this, &MinecraftUpdate::propogateStepProgress);
disconnect(task.get(), &Task::status, this, &MinecraftUpdate::setStatus);
+ disconnect(task.get(), &Task::details, this, &MinecraftUpdate::setDetails);
}
if(m_currentTask == m_tasks.size())
{
@@ -118,7 +120,9 @@ void MinecraftUpdate::next()
connect(task.get(), &Task::failed, this, &MinecraftUpdate::subtaskFailed);
connect(task.get(), &Task::aborted, this, &Task::abort);
connect(task.get(), &Task::progress, this, &MinecraftUpdate::progress);
+ connect(task.get(), &Task::stepProgress, this, &MinecraftUpdate::propogateStepProgress);
connect(task.get(), &Task::status, this, &MinecraftUpdate::setStatus);
+ connect(task.get(), &Task::details, this, &MinecraftUpdate::setDetails);
// if the task is already running, do not start it again
if(!task->isRunning())
{
diff --git a/launcher/minecraft/OneSixVersionFormat.cpp b/launcher/minecraft/OneSixVersionFormat.cpp
index 888b6860..b586198b 100644
--- a/launcher/minecraft/OneSixVersionFormat.cpp
+++ b/launcher/minecraft/OneSixVersionFormat.cpp
@@ -276,7 +276,7 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument &doc
if (root.contains("requires"))
{
- Meta::parseRequires(root, &out->requires);
+ Meta::parseRequires(root, &out->m_requires);
}
QString dependsOnMinecraftVersion = root.value("mcVersion").toString();
if(!dependsOnMinecraftVersion.isEmpty())
@@ -284,9 +284,9 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument &doc
Meta::Require mcReq;
mcReq.uid = "net.minecraft";
mcReq.equalsVersion = dependsOnMinecraftVersion;
- if (out->requires.count(mcReq) == 0)
+ if (out->m_requires.count(mcReq) == 0)
{
- out->requires.insert(mcReq);
+ out->m_requires.insert(mcReq);
}
}
if (root.contains("conflicts"))
@@ -392,9 +392,9 @@ QJsonDocument OneSixVersionFormat::versionFileToJson(const VersionFilePtr &patch
}
root.insert("mods", array);
}
- if(!patch->requires.empty())
+ if(!patch->m_requires.empty())
{
- Meta::serializeRequires(root, &patch->requires, "requires");
+ Meta::serializeRequires(root, &patch->m_requires, "requires");
}
if(!patch->conflicts.empty())
{
diff --git a/launcher/minecraft/PackProfile.cpp b/launcher/minecraft/PackProfile.cpp
index aff05dbc..e8fd2157 100644
--- a/launcher/minecraft/PackProfile.cpp
+++ b/launcher/minecraft/PackProfile.cpp
@@ -65,7 +65,8 @@
static const QMap<QString, ResourceAPI::ModLoaderType> modloaderMapping{
{"net.minecraftforge", ResourceAPI::Forge},
{"net.fabricmc.fabric-loader", ResourceAPI::Fabric},
- {"org.quiltmc.quilt-loader", ResourceAPI::Quilt}
+ {"org.quiltmc.quilt-loader", ResourceAPI::Quilt},
+ {"com.mumfrey.liteloader", ResourceAPI::LiteLoader}
};
PackProfile::PackProfile(MinecraftInstance * instance)
diff --git a/launcher/minecraft/VersionFile.h b/launcher/minecraft/VersionFile.h
index 11c5a3af..8e9dd167 100644
--- a/launcher/minecraft/VersionFile.h
+++ b/launcher/minecraft/VersionFile.h
@@ -138,7 +138,7 @@ public: /* data */
* Prism Launcher: set of packages this depends on
* NOTE: this is shared with the meta format!!!
*/
- Meta::RequireSet requires;
+ Meta::RequireSet m_requires;
/**
* Prism Launcher: set of packages this conflicts with
diff --git a/launcher/minecraft/World.cpp b/launcher/minecraft/World.cpp
index d310f8b9..54fb9434 100644
--- a/launcher/minecraft/World.cpp
+++ b/launcher/minecraft/World.cpp
@@ -56,6 +56,8 @@
#include <optional>
+#include "FileSystem.h"
+
using std::optional;
using std::nullopt;
@@ -567,3 +569,25 @@ bool World::operator==(const World &other) const
{
return is_valid == other.is_valid && folderName() == other.folderName();
}
+
+bool World::isSymLinkUnder(const QString& instPath) const
+{
+ if (isSymLink())
+ return true;
+
+ auto instDir = QDir(instPath);
+
+ auto relAbsPath = instDir.relativeFilePath(m_containerFile.absoluteFilePath());
+ auto relCanonPath = instDir.relativeFilePath(m_containerFile.canonicalFilePath());
+
+ return relAbsPath != relCanonPath;
+}
+
+bool World::isMoreThanOneHardLink() const
+{
+ if (m_containerFile.isDir())
+ {
+ return FS::hardLinkCount(QDir(m_containerFile.absoluteFilePath()).filePath("level.dat")) > 1;
+ }
+ return FS::hardLinkCount(m_containerFile.absoluteFilePath()) > 1;
+}
diff --git a/launcher/minecraft/World.h b/launcher/minecraft/World.h
index 8327253a..10328cce 100644
--- a/launcher/minecraft/World.h
+++ b/launcher/minecraft/World.h
@@ -95,6 +95,21 @@ public:
// WEAK compare operator - used for replacing worlds
bool operator==(const World &other) const;
+ [[nodiscard]] auto isSymLink() const -> bool{ return m_containerFile.isSymLink(); }
+
+ /**
+ * @brief Take a instance path, checks if the file pointed to by the resource is a symlink or under a symlink in that instance
+ *
+ * @param instPath path to an instance directory
+ * @return true
+ * @return false
+ */
+ [[nodiscard]] bool isSymLinkUnder(const QString& instPath) const;
+
+ [[nodiscard]] bool isMoreThanOneHardLink() const;
+
+ QString canonicalFilePath() const { return m_containerFile.canonicalFilePath(); }
+
private:
void readFromZip(const QFileInfo &file);
void readFromFS(const QFileInfo &file);
diff --git a/launcher/minecraft/WorldList.cpp b/launcher/minecraft/WorldList.cpp
index ae29a972..0feee299 100644
--- a/launcher/minecraft/WorldList.cpp
+++ b/launcher/minecraft/WorldList.cpp
@@ -45,16 +45,15 @@
#include <QFileSystemWatcher>
#include <QDebug>
-WorldList::WorldList(const QString &dir)
- : QAbstractListModel(), m_dir(dir)
+WorldList::WorldList(const QString &dir, BaseInstance* instance)
+ : QAbstractListModel(), m_instance(instance), m_dir(dir)
{
FS::ensureFolderPathExists(m_dir.absolutePath());
m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs);
m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware);
m_watcher = new QFileSystemWatcher(this);
is_watching = false;
- connect(m_watcher, SIGNAL(directoryChanged(QString)), this,
- SLOT(directoryChanged(QString)));
+ connect(m_watcher, &QFileSystemWatcher::directoryChanged, this, &WorldList::directoryChanged);
}
void WorldList::startWatching()
@@ -128,6 +127,10 @@ bool WorldList::isValid()
return m_dir.exists() && m_dir.isReadable();
}
+QString WorldList::instDirPath() const {
+ return QFileInfo(m_instance->instanceRoot()).absoluteFilePath();
+}
+
bool WorldList::deleteWorld(int index)
{
if (index >= worlds.size() || index < 0)
@@ -173,7 +176,7 @@ bool WorldList::resetIcon(int row)
int WorldList::columnCount(const QModelIndex &parent) const
{
- return parent.isValid()? 0 : 4;
+ return parent.isValid()? 0 : 5;
}
QVariant WorldList::data(const QModelIndex &index, int role) const
@@ -207,6 +210,14 @@ QVariant WorldList::data(const QModelIndex &index, int role) const
case SizeColumn:
return locale.formattedDataSize(world.bytes());
+ case InfoColumn:
+ if (world.isSymLinkUnder(instDirPath())) {
+ return tr("This world is symbolically linked from elsewhere.");
+ }
+ if (world.isMoreThanOneHardLink()) {
+ return tr("\nThis world is hard linked elsewhere.");
+ }
+ return "";
default:
return QVariant();
}
@@ -222,7 +233,16 @@ QVariant WorldList::data(const QModelIndex &index, int role) const
}
case Qt::ToolTipRole:
- {
+ {
+ if (column == InfoColumn) {
+ if (world.isSymLinkUnder(instDirPath())) {
+ return tr("Warning: This world is symbolically linked from elsewhere. Editing it will also change the original."
+ "\nCanonical Path: %1").arg(world.canonicalFilePath());
+ }
+ if (world.isMoreThanOneHardLink()) {
+ return tr("Warning: This world is hard linked elsewhere. Editing it will also change the original.");
+ }
+ }
return world.folderName();
}
case ObjectRole:
@@ -274,6 +294,9 @@ QVariant WorldList::headerData(int section, Qt::Orientation orientation, int rol
case SizeColumn:
//: World size on disk
return tr("Size");
+ case InfoColumn:
+ //: special warnings?
+ return tr("Info");
default:
return QVariant();
}
@@ -289,6 +312,8 @@ QVariant WorldList::headerData(int section, Qt::Orientation orientation, int rol
return tr("Date and time the world was last played.");
case SizeColumn:
return tr("Size of the world on disk.");
+ case InfoColumn:
+ return tr("Information and warnings about the world.");
default:
return QVariant();
}
diff --git a/launcher/minecraft/WorldList.h b/launcher/minecraft/WorldList.h
index 08294755..96b64193 100644
--- a/launcher/minecraft/WorldList.h
+++ b/launcher/minecraft/WorldList.h
@@ -21,6 +21,7 @@
#include <QAbstractListModel>
#include <QMimeData>
#include "minecraft/World.h"
+#include "BaseInstance.h"
class QFileSystemWatcher;
@@ -33,7 +34,8 @@ public:
NameColumn,
GameModeColumn,
LastPlayedColumn,
- SizeColumn
+ SizeColumn,
+ InfoColumn
};
enum Roles
@@ -48,7 +50,7 @@ public:
IconFileRole
};
- WorldList(const QString &dir);
+ WorldList(const QString &dir, BaseInstance* instance);
virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
@@ -112,6 +114,8 @@ public:
return m_dir;
}
+ QString instDirPath() const;
+
const QList<World> &allWorlds() const
{
return worlds;
@@ -124,6 +128,7 @@ signals:
void changed();
protected:
+ BaseInstance* m_instance;
QFileSystemWatcher *m_watcher;
bool is_watching;
QDir m_dir;
diff --git a/launcher/minecraft/auth/AccountList.cpp b/launcher/minecraft/auth/AccountList.cpp
index 9e2fd111..d6f42b75 100644
--- a/launcher/minecraft/auth/AccountList.cpp
+++ b/launcher/minecraft/auth/AccountList.cpp
@@ -328,18 +328,21 @@ QVariant AccountList::data(const QModelIndex &index, int role) const
case AccountState::Gone: {
return tr("Gone", "Account status");
}
+ default: {
+ return tr("Unknown", "Account status");
+ }
}
}
case MigrationColumn: {
if(account->isMSA() || account->isOffline()) {
- return tr("N/A", "Can Migrate?");
+ return tr("N/A", "Can Migrate");
}
if (account->canMigrate()) {
- return tr("Yes", "Can Migrate?");
+ return tr("Yes", "Can Migrate");
}
else {
- return tr("No", "Can Migrate?");
+ return tr("No", "Can Migrate");
}
}
@@ -354,11 +357,12 @@ QVariant AccountList::data(const QModelIndex &index, int role) const
return QVariant::fromValue(account);
case Qt::CheckStateRole:
- switch (index.column())
- {
- case ProfileNameColumn:
- return account == m_defaultAccount ? Qt::Checked : Qt::Unchecked;
+ if (index.column() == ProfileNameColumn) {
+ return account == m_defaultAccount ? Qt::Checked : Qt::Unchecked;
+ } else {
+ return QVariant();
}
+
default:
return QVariant();
diff --git a/launcher/minecraft/auth/AuthRequest.cpp b/launcher/minecraft/auth/AuthRequest.cpp
index bb82e1e2..a21634b7 100644
--- a/launcher/minecraft/auth/AuthRequest.cpp
+++ b/launcher/minecraft/auth/AuthRequest.cpp
@@ -55,12 +55,12 @@ void AuthRequest::get(const QNetworkRequest &req, int timeout/* = 60*1000*/) {
reply_ = APPLICATION->network()->get(request_);
status_ = Requesting;
timedReplies_.add(new Katabasis::Reply(reply_, timeout));
-#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
- connect(reply_, SIGNAL(errorOccurred(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError)));
-#else
- connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError)));
+#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15
+ connect(reply_, &QNetworkReply::errorOccurred, this, &AuthRequest::onRequestError);
+#else // &QNetworkReply::error SIGNAL depricated
+ connect(reply_, QOverload<QNetworkReply::NetworkError>::of(&QNetworkReply::error), this, &AuthRequest::onRequestError);
#endif
- connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished()));
+ connect(reply_, &QNetworkReply::finished, this, &AuthRequest::onRequestFinished);
connect(reply_, &QNetworkReply::sslErrors, this, &AuthRequest::onSslErrors);
}
@@ -70,14 +70,14 @@ void AuthRequest::post(const QNetworkRequest &req, const QByteArray &data, int t
status_ = Requesting;
reply_ = APPLICATION->network()->post(request_, data_);
timedReplies_.add(new Katabasis::Reply(reply_, timeout));
-#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
- connect(reply_, SIGNAL(errorOccurred(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError)));
-#else
- connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError)));
+#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15
+ connect(reply_, &QNetworkReply::errorOccurred, this, &AuthRequest::onRequestError);
+#else // &QNetworkReply::error SIGNAL depricated
+ connect(reply_, QOverload<QNetworkReply::NetworkError>::of(&QNetworkReply::error), this, &AuthRequest::onRequestError);
#endif
- connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished()));
+ connect(reply_, &QNetworkReply::finished, this, &AuthRequest::onRequestFinished);
connect(reply_, &QNetworkReply::sslErrors, this, &AuthRequest::onSslErrors);
- connect(reply_, SIGNAL(uploadProgress(qint64,qint64)), this, SLOT(onUploadProgress(qint64,qint64)));
+ connect(reply_, &QNetworkReply::uploadProgress, this, &AuthRequest::onUploadProgress);
}
void AuthRequest::onRequestFinished() {
diff --git a/launcher/minecraft/auth/MinecraftAccount.cpp b/launcher/minecraft/auth/MinecraftAccount.cpp
index 48cf5d42..3b050ac0 100644
--- a/launcher/minecraft/auth/MinecraftAccount.cpp
+++ b/launcher/minecraft/auth/MinecraftAccount.cpp
@@ -133,8 +133,8 @@ shared_qobject_ptr<AccountTask> MinecraftAccount::login(QString password) {
Q_ASSERT(m_currentTask.get() == nullptr);
m_currentTask.reset(new MojangLogin(&data, password));
- connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded()));
- connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString)));
+ connect(m_currentTask.get(), &Task::succeeded, this, &MinecraftAccount::authSucceeded);
+ connect(m_currentTask.get(), &Task::failed, this, &MinecraftAccount::authFailed);
connect(m_currentTask.get(), &Task::aborted, this, [this]{ authFailed(tr("Aborted")); });
emit activityChanged(true);
return m_currentTask;
@@ -144,8 +144,8 @@ shared_qobject_ptr<AccountTask> MinecraftAccount::loginMSA() {
Q_ASSERT(m_currentTask.get() == nullptr);
m_currentTask.reset(new MSAInteractive(&data));
- connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded()));
- connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString)));
+ connect(m_currentTask.get(), &Task::succeeded, this, &MinecraftAccount::authSucceeded);
+ connect(m_currentTask.get(), &Task::failed, this, &MinecraftAccount::authFailed);
connect(m_currentTask.get(), &Task::aborted, this, [this]{ authFailed(tr("Aborted")); });
emit activityChanged(true);
return m_currentTask;
@@ -155,8 +155,8 @@ shared_qobject_ptr<AccountTask> MinecraftAccount::loginOffline() {
Q_ASSERT(m_currentTask.get() == nullptr);
m_currentTask.reset(new OfflineLogin(&data));
- connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded()));
- connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString)));
+ connect(m_currentTask.get(), &Task::succeeded, this, &MinecraftAccount::authSucceeded);
+ connect(m_currentTask.get(), &Task::failed, this, &MinecraftAccount::authFailed);
connect(m_currentTask.get(), &Task::aborted, this, [this]{ authFailed(tr("Aborted")); });
emit activityChanged(true);
return m_currentTask;
@@ -177,8 +177,8 @@ shared_qobject_ptr<AccountTask> MinecraftAccount::refresh() {
m_currentTask.reset(new MojangRefresh(&data));
}
- connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded()));
- connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString)));
+ connect(m_currentTask.get(), &Task::succeeded, this, &MinecraftAccount::authSucceeded);
+ connect(m_currentTask.get(), &Task::failed, this, &MinecraftAccount::authFailed);
connect(m_currentTask.get(), &Task::aborted, this, [this]{ authFailed(tr("Aborted")); });
emit activityChanged(true);
return m_currentTask;
diff --git a/launcher/minecraft/auth/Yggdrasil.cpp b/launcher/minecraft/auth/Yggdrasil.cpp
index 29978411..d3e7ccdd 100644
--- a/launcher/minecraft/auth/Yggdrasil.cpp
+++ b/launcher/minecraft/auth/Yggdrasil.cpp
@@ -273,6 +273,7 @@ void Yggdrasil::processReply() {
AccountTaskState::STATE_FAILED_GONE,
tr("The Mojang account no longer exists. It may have been migrated to a Microsoft account.")
);
+ return;
}
default:
changeState(
diff --git a/launcher/minecraft/auth/steps/XboxUserStep.cpp b/launcher/minecraft/auth/steps/XboxUserStep.cpp
index 53069597..842eb60f 100644
--- a/launcher/minecraft/auth/steps/XboxUserStep.cpp
+++ b/launcher/minecraft/auth/steps/XboxUserStep.cpp
@@ -38,6 +38,10 @@ void XboxUserStep::perform() {
QNetworkRequest request = QNetworkRequest(QUrl("https://user.auth.xboxlive.com/user/authenticate"));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("Accept", "application/json");
+ // set contract-verison header (prevent err 400 bad-request?)
+ // https://learn.microsoft.com/en-us/gaming/gdk/_content/gc/reference/live/rest/additional/httpstandardheaders
+ request.setRawHeader("x-xbl-contract-version", "1");
+
auto *requestor = new AuthRequest(this);
connect(requestor, &AuthRequest::finished, this, &XboxUserStep::onRequestDone);
requestor->post(request, xbox_auth_data.toUtf8());
diff --git a/launcher/minecraft/mod/DataPack.cpp b/launcher/minecraft/mod/DataPack.cpp
index 5c58f6b2..c5754638 100644
--- a/launcher/minecraft/mod/DataPack.cpp
+++ b/launcher/minecraft/mod/DataPack.cpp
@@ -33,7 +33,9 @@ static const QMap<int, std::pair<Version, Version>> s_pack_format_versions = {
{ 4, { Version("1.13"), Version("1.14.4") } }, { 5, { Version("1.15"), Version("1.16.1") } },
{ 6, { Version("1.16.2"), Version("1.16.5") } }, { 7, { Version("1.17"), Version("1.17.1") } },
{ 8, { Version("1.18"), Version("1.18.1") } }, { 9, { Version("1.18.2"), Version("1.18.2") } },
- { 10, { Version("1.19"), Version("1.19.3") } },
+ { 10, { Version("1.19"), Version("1.19.3") } }, { 11, { Version("23w03a"), Version("23w05a") } },
+ { 12, { Version("1.19.4"), Version("1.19.4") } }, { 13, { Version("23w12a"), Version("23w14a") } },
+ { 14, { Version("23w16a"), Version("23w17a") } }, { 15, { Version("1.20"), Version("1.20") } },
};
void DataPack::setPackFormat(int new_format_id)
@@ -72,6 +74,7 @@ std::pair<int, bool> DataPack::compare(const Resource& other, SortType type) con
auto res = Resource::compare(other, type);
if (res.first != 0)
return res;
+ break;
}
case SortType::PACK_FORMAT: {
auto this_ver = packFormat();
@@ -81,6 +84,7 @@ std::pair<int, bool> DataPack::compare(const Resource& other, SortType type) con
return { 1, type == SortType::PACK_FORMAT };
if (this_ver < other_ver)
return { -1, type == SortType::PACK_FORMAT };
+ break;
}
}
return { 0, false };
diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp
index c495cd47..880dacb1 100644
--- a/launcher/minecraft/mod/Mod.cpp
+++ b/launcher/minecraft/mod/Mod.cpp
@@ -41,9 +41,11 @@
#include <QString>
#include <QRegularExpression>
+#include "MTPixmapCache.h"
#include "MetadataHandler.h"
#include "Version.h"
#include "minecraft/mod/ModDetails.h"
+#include "minecraft/mod/tasks/LocalModParseTask.h"
static ModPlatform::ProviderCapabilities ProviderCaps;
@@ -89,6 +91,7 @@ std::pair<int, bool> Mod::compare(const Resource& other, SortType type) const
auto res = Resource::compare(other, type);
if (res.first != 0)
return res;
+ break;
}
case SortType::VERSION: {
auto this_ver = Version(version());
@@ -97,11 +100,13 @@ std::pair<int, bool> Mod::compare(const Resource& other, SortType type) const
return { 1, type == SortType::VERSION };
if (this_ver < other_ver)
return { -1, type == SortType::VERSION };
+ break;
}
case SortType::PROVIDER: {
auto compare_result = QString::compare(provider().value_or("Unknown"), cast_other->provider().value_or("Unknown"), Qt::CaseInsensitive);
if (compare_result != 0)
return { compare_result, type == SortType::PROVIDER };
+ break;
}
}
return { 0, false };
@@ -121,7 +126,7 @@ bool Mod::applyFilter(QRegularExpression filter) const
return Resource::applyFilter(filter);
}
-auto Mod::destroy(QDir& index_dir, bool preserve_metadata) -> bool
+auto Mod::destroy(QDir& index_dir, bool preserve_metadata, bool attempt_trash) -> bool
{
if (!preserve_metadata) {
qDebug() << QString("Destroying metadata for '%1' on purpose").arg(name());
@@ -134,7 +139,7 @@ auto Mod::destroy(QDir& index_dir, bool preserve_metadata) -> bool
}
}
- return Resource::destroy();
+ return Resource::destroy(attempt_trash);
}
auto Mod::details() const -> const ModDetails&
@@ -164,6 +169,13 @@ auto Mod::homeurl() const -> QString
return details().homeurl;
}
+auto Mod::metaurl() const -> QString
+{
+ if (metadata() == nullptr)
+ return homeurl();
+ return ModPlatform::getMetaURL(metadata()->provider, metadata()->project_id);
+}
+
auto Mod::description() const -> QString
{
return details().description;
@@ -201,7 +213,10 @@ void Mod::finishResolvingWithDetails(ModDetails&& details)
m_local_details = std::move(details);
if (metadata)
setMetadata(std::move(metadata));
-};
+ if (!iconPath().isEmpty()) {
+ m_pack_image_cache_key.was_read_attempt = false;
+ }
+}
auto Mod::provider() const -> std::optional<QString>
{
@@ -210,6 +225,56 @@ auto Mod::provider() const -> std::optional<QString>
return {};
}
+auto Mod::licenses() const -> const QList<ModLicense>&
+{
+ return details().licenses;
+}
+
+ auto Mod::issueTracker() const -> QString
+{
+ return details().issue_tracker;
+}
+
+void Mod::setIcon(QImage new_image) const
+{
+ QMutexLocker locker(&m_data_lock);
+
+ Q_ASSERT(!new_image.isNull());
+
+ if (m_pack_image_cache_key.key.isValid())
+ PixmapCache::remove(m_pack_image_cache_key.key);
+
+ // scale the image to avoid flooding the pixmapcache
+ auto pixmap = QPixmap::fromImage(new_image.scaled({64, 64}, Qt::AspectRatioMode::KeepAspectRatioByExpanding));
+
+ m_pack_image_cache_key.key = PixmapCache::insert(pixmap);
+ m_pack_image_cache_key.was_ever_used = true;
+ m_pack_image_cache_key.was_read_attempt = true;
+}
+
+QPixmap Mod::icon(QSize size, Qt::AspectRatioMode mode) const
+{
+ QPixmap cached_image;
+ if (PixmapCache::find(m_pack_image_cache_key.key, &cached_image)) {
+ if (size.isNull())
+ return cached_image;
+ return cached_image.scaled(size, mode);
+ }
+
+ // No valid image we can get
+ if ((!m_pack_image_cache_key.was_ever_used && m_pack_image_cache_key.was_read_attempt) || iconPath().isEmpty())
+ return {};
+
+ if (m_pack_image_cache_key.was_ever_used) {
+ qDebug() << "Mod" << name() << "Had it's icon evicted form the cache. reloading...";
+ PixmapCache::markCacheMissByEviciton();
+ }
+ // Image got evicted from the cache or an attempt to load it has not been made. load it and retry.
+ m_pack_image_cache_key.was_read_attempt = true;
+ ModUtils::loadIconFile(*this);
+ return icon(size);
+}
+
bool Mod::valid() const
{
return !m_local_details.mod_id.isEmpty();
diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h
index c4032538..b67bd465 100644
--- a/launcher/minecraft/mod/Mod.h
+++ b/launcher/minecraft/mod/Mod.h
@@ -38,6 +38,10 @@
#include <QDateTime>
#include <QFileInfo>
#include <QList>
+#include <QImage>
+#include <QMutex>
+#include <QPixmap>
+#include <QPixmapCache>
#include <optional>
@@ -64,6 +68,16 @@ public:
auto authors() const -> QStringList;
auto status() const -> ModStatus;
auto provider() const -> std::optional<QString>;
+ auto licenses() const -> const QList<ModLicense>&;
+ auto issueTracker() const -> QString;
+ auto metaurl() const -> QString;
+
+ /** Get the intneral path to the mod's icon file*/
+ QString iconPath() const { return m_local_details.icon_file; };
+ /** Gets the icon of the mod, converted to a QPixmap for drawing, and scaled to size. */
+ [[nodiscard]] QPixmap icon(QSize size, Qt::AspectRatioMode mode = Qt::AspectRatioMode::IgnoreAspectRatio) const;
+ /** Thread-safe. */
+ void setIcon(QImage new_image) const;
auto metadata() -> std::shared_ptr<Metadata::ModStruct>;
auto metadata() const -> const std::shared_ptr<Metadata::ModStruct>;
@@ -79,10 +93,19 @@ public:
[[nodiscard]] bool applyFilter(QRegularExpression filter) const override;
// Delete all the files of this mod
- auto destroy(QDir& index_dir, bool preserve_metadata = false) -> bool;
+ auto destroy(QDir& index_dir, bool preserve_metadata = false, bool attempt_trash = true) -> bool;
void finishResolvingWithDetails(ModDetails&& details);
protected:
ModDetails m_local_details;
+
+ mutable QMutex m_data_lock;
+
+ struct {
+ QPixmapCache::Key key;
+ bool was_ever_used = false;
+ bool was_read_attempt = false;
+ } mutable m_pack_image_cache_key;
+
};
diff --git a/launcher/minecraft/mod/ModDetails.h b/launcher/minecraft/mod/ModDetails.h
index 176e4fc1..b4e59d52 100644
--- a/launcher/minecraft/mod/ModDetails.h
+++ b/launcher/minecraft/mod/ModDetails.h
@@ -39,6 +39,7 @@
#include <QString>
#include <QStringList>
+#include <QUrl>
#include "minecraft/mod/MetadataHandler.h"
@@ -49,6 +50,84 @@ enum class ModStatus {
Unknown, // Default status
};
+struct ModLicense {
+ QString name = {};
+ QString id = {};
+ QString url = {};
+ QString description = {};
+
+ ModLicense() {}
+
+ ModLicense(const QString license) {
+ // FIXME: come up with a better license parseing.
+ // handle SPDX identifiers? https://spdx.org/licenses/
+ auto parts = license.split(' ');
+ QStringList notNameParts = {};
+ for (auto part : parts) {
+ auto url = QUrl(part);
+ if (part.startsWith("(") && part.endsWith(")"))
+ url = QUrl(part.mid(1, part.size() - 2));
+
+ if (url.isValid() && !url.scheme().isEmpty() && !url.host().isEmpty()) {
+ this->url = url.toString();
+ notNameParts.append(part);
+ continue;
+ }
+ }
+
+ for (auto part : notNameParts) {
+ parts.removeOne(part);
+ }
+
+ auto licensePart = parts.join(' ');
+ this->name = licensePart;
+ this->description = licensePart;
+
+ if (parts.size() == 1) {
+ this->id = parts.first();
+ }
+
+ }
+
+ ModLicense(const QString name, const QString id, const QString url, const QString description) {
+ this->name = name;
+ this->id = id;
+ this->url = url;
+ this->description = description;
+ }
+
+ ModLicense(const ModLicense& other)
+ : name(other.name)
+ , id(other.id)
+ , url(other.url)
+ , description(other.description)
+ {}
+
+ ModLicense& operator=(const ModLicense& other)
+ {
+ this->name = other.name;
+ this->id = other.id;
+ this->url = other.url;
+ this->description = other.description;
+
+ return *this;
+ }
+
+ ModLicense& operator=(const ModLicense&& other)
+ {
+ this->name = other.name;
+ this->id = other.id;
+ this->url = other.url;
+ this->description = other.description;
+
+ return *this;
+ }
+
+ bool isEmpty() {
+ return this->name.isEmpty() && this->id.isEmpty() && this->url.isEmpty() && this->description.isEmpty();
+ }
+};
+
struct ModDetails
{
/* Mod ID as defined in the ModLoader-specific metadata */
@@ -72,6 +151,15 @@ struct ModDetails
/* List of the author's names */
QStringList authors = {};
+ /* Issue Tracker URL */
+ QString issue_tracker = {};
+
+ /* License */
+ QList<ModLicense> licenses = {};
+
+ /* Path of mod logo */
+ QString icon_file = {};
+
/* Installation status of the mod */
ModStatus status = ModStatus::Unknown;
@@ -89,6 +177,9 @@ struct ModDetails
, homeurl(other.homeurl)
, description(other.description)
, authors(other.authors)
+ , issue_tracker(other.issue_tracker)
+ , licenses(other.licenses)
+ , icon_file(other.icon_file)
, status(other.status)
{}
@@ -101,6 +192,9 @@ struct ModDetails
this->homeurl = other.homeurl;
this->description = other.description;
this->authors = other.authors;
+ this->issue_tracker = other.issue_tracker;
+ this->licenses = other.licenses;
+ this->icon_file = other.icon_file;
this->status = other.status;
return *this;
@@ -115,6 +209,9 @@ struct ModDetails
this->homeurl = other.homeurl;
this->description = other.description;
this->authors = other.authors;
+ this->issue_tracker = other.issue_tracker;
+ this->licenses = other.licenses;
+ this->icon_file = other.icon_file;
this->status = other.status;
return *this;
diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp
index 3f31b93c..51383edf 100644
--- a/launcher/minecraft/mod/ModFolderModel.cpp
+++ b/launcher/minecraft/mod/ModFolderModel.cpp
@@ -37,22 +37,30 @@
#include "ModFolderModel.h"
#include <FileSystem.h>
+#include <qheaderview.h>
#include <QDebug>
#include <QFileSystemWatcher>
+#include <QIcon>
#include <QMimeData>
#include <QString>
+#include <QStyle>
#include <QThreadPool>
#include <QUrl>
#include <QUuid>
#include <algorithm>
+#include "Application.h"
+
#include "minecraft/mod/tasks/LocalModParseTask.h"
#include "minecraft/mod/tasks/ModFolderLoadTask.h"
-#include "modplatform/ModIndex.h"
-ModFolderModel::ModFolderModel(const QString &dir, bool is_indexed, bool create_dir) : ResourceFolderModel(QDir(dir), nullptr, create_dir), m_is_indexed(is_indexed)
+ModFolderModel::ModFolderModel(const QString& dir, BaseInstance* instance, bool is_indexed, bool create_dir)
+ : ResourceFolderModel(QDir(dir), instance, nullptr, create_dir), m_is_indexed(is_indexed)
{
- m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::VERSION, SortType::DATE, SortType::PROVIDER };
+ m_column_names = QStringList({ "Enable", "Image", "Name", "Version", "Last Modified", "Provider" });
+ m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Version"), tr("Last Modified"), tr("Provider") });
+ m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME , SortType::VERSION, SortType::DATE, SortType::PROVIDER};
+ m_column_resize_modes = { QHeaderView::ResizeToContents, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::ResizeToContents, QHeaderView::ResizeToContents, QHeaderView::ResizeToContents};
}
QVariant ModFolderModel::data(const QModelIndex &index, int role) const
@@ -97,8 +105,27 @@ QVariant ModFolderModel::data(const QModelIndex &index, int role) const
}
case Qt::ToolTipRole:
+ if (column == NAME_COLUMN) {
+ if (at(row)->isSymLinkUnder(instDirPath())) {
+ return m_resources[row]->internal_id() +
+ tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original."
+ "\nCanonical Path: %1")
+ .arg(at(row)->fileinfo().canonicalFilePath());
+ }
+ if (at(row)->isMoreThanOneHardLink()) {
+ return m_resources[row]->internal_id() +
+ tr("\nWarning: This resource is hard linked elsewhere. Editing it will also change the original.");
+ }
+ }
return m_resources[row]->internal_id();
-
+ case Qt::DecorationRole: {
+ if (column == NAME_COLUMN && (at(row)->isSymLinkUnder(instDirPath()) || at(row)->isMoreThanOneHardLink()))
+ return APPLICATION->getThemedIcon("status-yellow");
+ if (column == ImageColumn) {
+ return at(row)->icon({32, 32}, Qt::AspectRatioMode::KeepAspectRatioByExpanding);
+ }
+ return {};
+ }
case Qt::CheckStateRole:
switch (column)
{
@@ -120,15 +147,12 @@ QVariant ModFolderModel::headerData(int section, Qt::Orientation orientation, in
switch (section)
{
case ActiveColumn:
- return QString();
case NameColumn:
- return tr("Name");
case VersionColumn:
- return tr("Version");
case DateColumn:
- return tr("Last changed");
case ProviderColumn:
- return tr("Provider");
+ case ImageColumn:
+ return columnNames().at(section);
default:
return QVariant();
}
@@ -175,10 +199,10 @@ Task* ModFolderModel::createParseTask(Resource& resource)
bool ModFolderModel::uninstallMod(const QString& filename, bool preserve_metadata)
{
- for(auto mod : allMods()){
- if(mod->fileinfo().fileName() == filename){
+ for(auto mod : allMods()) {
+ if(mod->fileinfo().fileName() == filename) {
auto index_dir = indexDir();
- mod->destroy(index_dir, preserve_metadata);
+ mod->destroy(index_dir, preserve_metadata, false);
update();
@@ -191,16 +215,11 @@ bool ModFolderModel::uninstallMod(const QString& filename, bool preserve_metadat
bool ModFolderModel::deleteMods(const QModelIndexList& indexes)
{
- if(!m_can_interact) {
- return false;
- }
-
- if(indexes.isEmpty())
+ if (indexes.isEmpty())
return true;
- for (auto i: indexes)
- {
- if(i.column() != 0) {
+ for (auto i : indexes) {
+ if (i.column() != 0) {
continue;
}
auto m = at(i.row());
diff --git a/launcher/minecraft/mod/ModFolderModel.h b/launcher/minecraft/mod/ModFolderModel.h
index 84e70db9..6ccaba23 100644
--- a/launcher/minecraft/mod/ModFolderModel.h
+++ b/launcher/minecraft/mod/ModFolderModel.h
@@ -64,6 +64,7 @@ public:
enum Columns
{
ActiveColumn = 0,
+ ImageColumn,
NameColumn,
VersionColumn,
DateColumn,
@@ -75,7 +76,9 @@ public:
Enable,
Toggle
};
- ModFolderModel(const QString &dir, bool is_indexed = false, bool create_dir = true);
+ ModFolderModel(const QString &dir, BaseInstance* instance, bool is_indexed = false, bool create_dir = true);
+
+ virtual QString id() const override { return "mods"; }
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
diff --git a/launcher/minecraft/mod/Resource.cpp b/launcher/minecraft/mod/Resource.cpp
index 0d35d755..098a617f 100644
--- a/launcher/minecraft/mod/Resource.cpp
+++ b/launcher/minecraft/mod/Resource.cpp
@@ -1,6 +1,8 @@
#include "Resource.h"
+
#include <QRegularExpression>
+#include <QFileInfo>
#include "FileSystem.h"
@@ -69,6 +71,7 @@ std::pair<int, bool> Resource::compare(const Resource& other, SortType type) con
return { 1, type == SortType::ENABLED };
if (!enabled() && other.enabled())
return { -1, type == SortType::ENABLED };
+ break;
case SortType::NAME: {
QString this_name{ name() };
QString other_name{ other.name() };
@@ -79,12 +82,14 @@ std::pair<int, bool> Resource::compare(const Resource& other, SortType type) con
auto compare_result = QString::compare(this_name, other_name, Qt::CaseInsensitive);
if (compare_result != 0)
return { compare_result, type == SortType::NAME };
+ break;
}
case SortType::DATE:
if (dateTimeChanged() > other.dateTimeChanged())
return { 1, type == SortType::DATE };
if (dateTimeChanged() < other.dateTimeChanged())
return { -1, type == SortType::DATE };
+ break;
}
return { 0, false };
@@ -143,12 +148,26 @@ bool Resource::enable(EnableAction action)
return true;
}
-bool Resource::destroy()
+bool Resource::destroy(bool attemptTrash)
{
m_type = ResourceType::UNKNOWN;
+ return (attemptTrash && FS::trash(m_file_info.filePath())) || FS::deletePath(m_file_info.filePath());
+}
- if (FS::trash(m_file_info.filePath()))
+bool Resource::isSymLinkUnder(const QString& instPath) const
+{
+ if (isSymLink())
return true;
- return FS::deletePath(m_file_info.filePath());
+ auto instDir = QDir(instPath);
+
+ auto relAbsPath = instDir.relativeFilePath(m_file_info.absoluteFilePath());
+ auto relCanonPath = instDir.relativeFilePath(m_file_info.canonicalFilePath());
+
+ return relAbsPath != relCanonPath;
+}
+
+bool Resource::isMoreThanOneHardLink() const
+{
+ return FS::hardLinkCount(m_file_info.absoluteFilePath()) > 1;
}
diff --git a/launcher/minecraft/mod/Resource.h b/launcher/minecraft/mod/Resource.h
index 0c37f3a3..94f3160c 100644
--- a/launcher/minecraft/mod/Resource.h
+++ b/launcher/minecraft/mod/Resource.h
@@ -92,7 +92,20 @@ class Resource : public QObject {
}
// Delete all files of this resource.
- bool destroy();
+ bool destroy(bool attemptTrash = true);
+
+ [[nodiscard]] auto isSymLink() const -> bool { return m_file_info.isSymLink(); }
+
+ /**
+ * @brief Take a instance path, checks if the file pointed to by the resource is a symlink or under a symlink in that instance
+ *
+ * @param instPath path to an instance directory
+ * @return true
+ * @return false
+ */
+ [[nodiscard]] bool isSymLinkUnder(const QString& instPath) const;
+
+ [[nodiscard]] bool isMoreThanOneHardLink() const;
protected:
/* The file corresponding to this resource. */
diff --git a/launcher/minecraft/mod/ResourceFolderModel.cpp b/launcher/minecraft/mod/ResourceFolderModel.cpp
index f2a77c12..39a61067 100644
--- a/launcher/minecraft/mod/ResourceFolderModel.cpp
+++ b/launcher/minecraft/mod/ResourceFolderModel.cpp
@@ -1,18 +1,28 @@
#include "ResourceFolderModel.h"
+#include <QMessageBox>
#include <QCoreApplication>
#include <QDebug>
+#include <QFileInfo>
+#include <QIcon>
+#include <QMenu>
#include <QMimeData>
+#include <QStyle>
#include <QThreadPool>
#include <QUrl>
+#include "Application.h"
#include "FileSystem.h"
+#include "QVariantUtils.h"
#include "minecraft/mod/tasks/BasicFolderLoadTask.h"
+#include "settings/Setting.h"
#include "tasks/Task.h"
+#include "ui/dialogs/CustomMessageBox.h"
-ResourceFolderModel::ResourceFolderModel(QDir dir, QObject* parent, bool create_dir) : QAbstractListModel(parent), m_dir(dir), m_watcher(this)
+ResourceFolderModel::ResourceFolderModel(QDir dir, BaseInstance* instance, QObject* parent, bool create_dir)
+ : QAbstractListModel(parent), m_dir(dir), m_instance(instance), m_watcher(this)
{
if (create_dir) {
FS::ensureFolderPathExists(m_dir.absolutePath());
@@ -22,7 +32,7 @@ ResourceFolderModel::ResourceFolderModel(QDir dir, QObject* parent, bool create_
m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware);
connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &ResourceFolderModel::directoryChanged);
- connect(&m_helper_thread_task, &ConcurrentTask::finished, this, [this]{ m_helper_thread_task.clear(); });
+ connect(&m_helper_thread_task, &ConcurrentTask::finished, this, [this] { m_helper_thread_task.clear(); });
}
ResourceFolderModel::~ResourceFolderModel()
@@ -69,10 +79,6 @@ bool ResourceFolderModel::stopWatching(const QStringList paths)
bool ResourceFolderModel::installResource(QString original_path)
{
- if (!m_can_interact) {
- return false;
- }
-
// NOTE: fix for GH-1178: remove trailing slash to avoid issues with using the empty result of QFileInfo::fileName
original_path = FS::NormalizePath(original_path);
QFileInfo file_info(original_path);
@@ -151,7 +157,7 @@ bool ResourceFolderModel::uninstallResource(QString file_name)
{
for (auto& resource : m_resources) {
if (resource->fileinfo().fileName() == file_name) {
- auto res = resource->destroy();
+ auto res = resource->destroy(false);
update();
@@ -163,9 +169,6 @@ bool ResourceFolderModel::uninstallResource(QString file_name)
bool ResourceFolderModel::deleteResources(const QModelIndexList& indexes)
{
- if (!m_can_interact)
- return false;
-
if (indexes.isEmpty())
return true;
@@ -184,11 +187,8 @@ bool ResourceFolderModel::deleteResources(const QModelIndexList& indexes)
return true;
}
-bool ResourceFolderModel::setResourceEnabled(const QModelIndexList &indexes, EnableAction action)
+bool ResourceFolderModel::setResourceEnabled(const QModelIndexList& indexes, EnableAction action)
{
- if (!m_can_interact)
- return false;
-
if (indexes.isEmpty())
return true;
@@ -241,15 +241,18 @@ bool ResourceFolderModel::update()
connect(m_current_update_task.get(), &Task::succeeded, this, &ResourceFolderModel::onUpdateSucceeded,
Qt::ConnectionType::QueuedConnection);
connect(m_current_update_task.get(), &Task::failed, this, &ResourceFolderModel::onUpdateFailed, Qt::ConnectionType::QueuedConnection);
- connect(m_current_update_task.get(), &Task::finished, this, [=] {
- m_current_update_task.reset();
- if (m_scheduled_update) {
- m_scheduled_update = false;
- update();
- } else {
- emit updateFinished();
- }
- }, Qt::ConnectionType::QueuedConnection);
+ connect(
+ m_current_update_task.get(), &Task::finished, this,
+ [=] {
+ m_current_update_task.reset();
+ if (m_scheduled_update) {
+ m_scheduled_update = false;
+ update();
+ } else {
+ emit updateFinished();
+ }
+ },
+ Qt::ConnectionType::QueuedConnection);
QThreadPool::globalInstance()->start(m_current_update_task.get());
@@ -339,15 +342,9 @@ Qt::DropActions ResourceFolderModel::supportedDropActions() const
Qt::ItemFlags ResourceFolderModel::flags(const QModelIndex& index) const
{
Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index);
- auto flags = defaultFlags;
- if (!m_can_interact) {
- flags &= ~Qt::ItemIsDropEnabled;
- } else {
- flags |= Qt::ItemIsDropEnabled;
- if (index.isValid()) {
- flags |= Qt::ItemIsUserCheckable;
- }
- }
+ auto flags = defaultFlags | Qt::ItemIsDropEnabled;
+ if (index.isValid())
+ flags |= Qt::ItemIsUserCheckable;
return flags;
}
@@ -417,7 +414,27 @@ QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const
return {};
}
case Qt::ToolTipRole:
+ if (column == NAME_COLUMN) {
+ if (at(row).isSymLinkUnder(instDirPath())) {
+ return m_resources[row]->internal_id() +
+ tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original."
+ "\nCanonical Path: %1")
+ .arg(at(row).fileinfo().canonicalFilePath());
+ ;
+ }
+ if (at(row).isMoreThanOneHardLink()) {
+ return m_resources[row]->internal_id() +
+ tr("\nWarning: This resource is hard linked elsewhere. Editing it will also change the original.");
+ }
+ }
+
return m_resources[row]->internal_id();
+ case Qt::DecorationRole: {
+ if (column == NAME_COLUMN && (at(row).isSymLinkUnder(instDirPath()) || at(row).isMoreThanOneHardLink()))
+ return APPLICATION->getThemedIcon("status-yellow");
+
+ return {};
+ }
case Qt::CheckStateRole:
switch (column) {
case ACTIVE_COLUMN:
@@ -436,8 +453,20 @@ bool ResourceFolderModel::setData(const QModelIndex& index, const QVariant& valu
if (row < 0 || row >= rowCount(index.parent()) || !index.isValid())
return false;
- if (role == Qt::CheckStateRole)
+ if (role == Qt::CheckStateRole) {
+ if (m_instance != nullptr && m_instance->isRunning()) {
+ auto response =
+ CustomMessageBox::selectable(nullptr, "Confirm toggle",
+ "If you enable/disable this resource while the game is running it may crash your game.\n"
+ "Are you sure you want to do this?",
+ QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No)
+ ->exec();
+
+ if (response != QMessageBox::Yes)
+ return false;
+ }
return setResourceEnabled({ index }, EnableAction::TOGGLE);
+ }
return false;
}
@@ -447,10 +476,10 @@ QVariant ResourceFolderModel::headerData(int section, Qt::Orientation orientatio
switch (role) {
case Qt::DisplayRole:
switch (section) {
+ case ACTIVE_COLUMN:
case NAME_COLUMN:
- return tr("Name");
case DATE_COLUMN:
- return tr("Last modified");
+ return columnNames().at(section);
default:
return {};
}
@@ -476,6 +505,75 @@ QVariant ResourceFolderModel::headerData(int section, Qt::Orientation orientatio
return {};
}
+void ResourceFolderModel::setupHeaderAction(QAction* act, int column)
+{
+ Q_ASSERT(act);
+
+ act->setText(columnNames().at(column));
+}
+
+void ResourceFolderModel::saveHiddenColumn(int column, bool hidden)
+{
+ auto const setting_name = QString("UI/%1_Page/HiddenColumns").arg(id());
+ auto setting = (m_instance->settings()->contains(setting_name)) ?
+ m_instance->settings()->getSetting(setting_name) : m_instance->settings()->registerSetting(setting_name);
+
+ auto hiddenColumns = setting->get().toStringList();
+ auto name = columnNames(false).at(column);
+ auto index = hiddenColumns.indexOf(name);
+ if (index >= 0 && !hidden) {
+ hiddenColumns.removeAt(index);
+ } else if ( index < 0 && hidden) {
+ hiddenColumns.append(name);
+ }
+ setting->set(hiddenColumns);
+}
+
+void ResourceFolderModel::loadHiddenColumns(QTreeView *tree)
+{
+ auto const setting_name = QString("UI/%1_Page/HiddenColumns").arg(id());
+ auto setting = (m_instance->settings()->contains(setting_name)) ?
+ m_instance->settings()->getSetting(setting_name) : m_instance->settings()->registerSetting(setting_name);
+
+ auto hiddenColumns = setting->get().toStringList();
+ auto col_names = columnNames(false);
+ for (auto col_name : hiddenColumns) {
+ auto index = col_names.indexOf(col_name);
+ if (index >= 0)
+ tree->setColumnHidden(index, true);
+ }
+
+}
+
+QMenu* ResourceFolderModel::createHeaderContextMenu(QTreeView* tree)
+{
+ auto menu = new QMenu(tree);
+
+ menu->addSeparator()->setText(tr("Show / Hide Columns"));
+
+ for (int col = 0; col < columnCount(); ++col) {
+ auto act = new QAction(menu);
+ setupHeaderAction(act, col);
+
+ act->setCheckable(true);
+ act->setChecked(!tree->isColumnHidden(col));
+
+ connect(act, &QAction::toggled, tree, [this, col, tree](bool toggled){
+ tree->setColumnHidden(col, !toggled);
+ for(int c = 0; c < columnCount(); ++c) {
+ if (m_column_resize_modes.at(c) == QHeaderView::ResizeToContents)
+ tree->resizeColumnToContents(c);
+ }
+ saveHiddenColumn(col, !toggled);
+ });
+
+ menu->addAction(act);
+
+ }
+
+ return menu;
+}
+
QSortFilterProxyModel* ResourceFolderModel::createFilterProxyModel(QObject* parent)
{
return new ProxyModel(parent);
@@ -487,16 +585,6 @@ SortType ResourceFolderModel::columnToSortKey(size_t column) const
return m_column_sort_keys.at(column);
}
-void ResourceFolderModel::enableInteraction(bool enabled)
-{
- if (m_can_interact == enabled)
- return;
-
- m_can_interact = enabled;
- if (size())
- emit dataChanged(index(0), index(size() - 1));
-}
-
/* Standard Proxy Model for createFilterProxyModel */
[[nodiscard]] bool ResourceFolderModel::ProxyModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const
{
@@ -531,3 +619,8 @@ void ResourceFolderModel::enableInteraction(bool enabled)
return (compare_result.first < 0);
return (compare_result.first > 0);
}
+
+QString ResourceFolderModel::instDirPath() const
+{
+ return QFileInfo(m_instance->instanceRoot()).absoluteFilePath();
+}
diff --git a/launcher/minecraft/mod/ResourceFolderModel.h b/launcher/minecraft/mod/ResourceFolderModel.h
index 3bd78870..454b84c3 100644
--- a/launcher/minecraft/mod/ResourceFolderModel.h
+++ b/launcher/minecraft/mod/ResourceFolderModel.h
@@ -1,5 +1,8 @@
#pragma once
+#include <QHeaderView>
+#include <QAction>
+#include <QTreeView>
#include <QAbstractListModel>
#include <QDir>
#include <QFileSystemWatcher>
@@ -9,8 +12,10 @@
#include "Resource.h"
-#include "tasks/Task.h"
+#include "BaseInstance.h"
+
#include "tasks/ConcurrentTask.h"
+#include "tasks/Task.h"
class QSortFilterProxyModel;
@@ -24,9 +29,11 @@ class QSortFilterProxyModel;
class ResourceFolderModel : public QAbstractListModel {
Q_OBJECT
public:
- ResourceFolderModel(QDir, QObject* parent = nullptr, bool create_dir = true);
+ ResourceFolderModel(QDir, BaseInstance* instance, QObject* parent = nullptr, bool create_dir = true);
~ResourceFolderModel() override;
+ virtual QString id() const { return "resource"; }
+
/** Starts watching the paths for changes.
*
* Returns whether starting to watch all the paths was successful.
@@ -90,6 +97,7 @@ class ResourceFolderModel : public QAbstractListModel {
/* Basic columns */
enum Columns { ACTIVE_COLUMN = 0, NAME_COLUMN, DATE_COLUMN, NUM_COLUMNS };
+ QStringList columnNames(bool translated = true) const { return translated ? m_column_names_translated : m_column_names; };
[[nodiscard]] int rowCount(const QModelIndex& parent = {}) const override { return parent.isValid() ? 0 : static_cast<int>(size()); }
[[nodiscard]] int columnCount(const QModelIndex& parent = {}) const override { return parent.isValid() ? 0 : NUM_COLUMNS; };
@@ -108,6 +116,11 @@ class ResourceFolderModel : public QAbstractListModel {
[[nodiscard]] QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
+ void setupHeaderAction(QAction* act, int column);
+ void saveHiddenColumn(int column, bool hidden);
+ void loadHiddenColumns(QTreeView* tree);
+ QMenu* createHeaderContextMenu(QTreeView* tree);
+
/** This creates a proxy model to filter / sort the model for a UI.
*
* The actual comparisons and filtering are done directly by the Resource, so to modify behavior go there instead!
@@ -115,6 +128,7 @@ class ResourceFolderModel : public QAbstractListModel {
QSortFilterProxyModel* createFilterProxyModel(QObject* parent = nullptr);
[[nodiscard]] SortType columnToSortKey(size_t column) const;
+ [[nodiscard]] QList<QHeaderView::ResizeMode> columnResizeModes() const { return m_column_resize_modes; }
class ProxyModel : public QSortFilterProxyModel {
public:
@@ -125,9 +139,7 @@ class ResourceFolderModel : public QAbstractListModel {
[[nodiscard]] bool lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const override;
};
- public slots:
- void enableInteraction(bool enabled);
- void disableInteraction(bool disabled) { enableInteraction(!disabled); }
+ QString instDirPath() const;
signals:
void updateFinished();
@@ -177,16 +189,22 @@ class ResourceFolderModel : public QAbstractListModel {
* if the resource is complex and has more stuff to parse.
*/
virtual void onParseSucceeded(int ticket, QString resource_id);
- virtual void onParseFailed(int ticket, QString resource_id) { Q_UNUSED(ticket); Q_UNUSED(resource_id); }
+ virtual void onParseFailed(int ticket, QString resource_id)
+ {
+ Q_UNUSED(ticket);
+ Q_UNUSED(resource_id);
+ }
protected:
// Represents the relationship between a column's index (represented by the list index), and it's sorting key.
// As such, the order in with they appear is very important!
QList<SortType> m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::DATE };
-
- bool m_can_interact = true;
+ QStringList m_column_names = {"Enable", "Name", "Last Modified"};
+ QStringList m_column_names_translated = {tr("Enable"), tr("Name"), tr("Last Modified")};
+ QList<QHeaderView::ResizeMode> m_column_resize_modes = { QHeaderView::ResizeToContents, QHeaderView::Stretch, QHeaderView::ResizeToContents };
QDir m_dir;
+ BaseInstance* m_instance;
QFileSystemWatcher m_watcher;
bool m_is_watching = false;
diff --git a/launcher/minecraft/mod/ResourcePack.cpp b/launcher/minecraft/mod/ResourcePack.cpp
index 876d5c3e..6d5978d4 100644
--- a/launcher/minecraft/mod/ResourcePack.cpp
+++ b/launcher/minecraft/mod/ResourcePack.cpp
@@ -18,7 +18,8 @@ static const QMap<int, std::pair<Version, Version>> s_pack_format_versions = {
{ 5, { Version("1.15"), Version("1.16.1") } }, { 6, { Version("1.16.2"), Version("1.16.5") } },
{ 7, { Version("1.17"), Version("1.17.1") } }, { 8, { Version("1.18"), Version("1.18.2") } },
{ 9, { Version("1.19"), Version("1.19.2") } }, { 11, { Version("22w42a"), Version("22w44a") } },
- { 12, { Version("1.19.3"), Version("1.19.3") } },
+ { 12, { Version("1.19.3"), Version("1.19.3") } }, { 13, { Version("1.19.4"), Version("1.19.4") } },
+ { 14, { Version("1.20"), Version("1.20") } }
};
void ResourcePack::setPackFormat(int new_format_id)
@@ -39,7 +40,7 @@ void ResourcePack::setDescription(QString new_description)
m_description = new_description;
}
-void ResourcePack::setImage(QImage new_image)
+void ResourcePack::setImage(QImage new_image) const
{
QMutexLocker locker(&m_data_lock);
@@ -48,7 +49,10 @@ void ResourcePack::setImage(QImage new_image)
if (m_pack_image_cache_key.key.isValid())
PixmapCache::instance().remove(m_pack_image_cache_key.key);
- m_pack_image_cache_key.key = PixmapCache::instance().insert(QPixmap::fromImage(new_image));
+ // scale the image to avoid flooding the pixmapcache
+ auto pixmap = QPixmap::fromImage(new_image.scaled({64, 64}, Qt::AspectRatioMode::KeepAspectRatioByExpanding));
+
+ m_pack_image_cache_key.key = PixmapCache::instance().insert(pixmap);
m_pack_image_cache_key.was_ever_used = true;
// This can happen if the pixmap is too big to fit in the cache :c
@@ -58,21 +62,25 @@ void ResourcePack::setImage(QImage new_image)
}
}
-QPixmap ResourcePack::image(QSize size)
+QPixmap ResourcePack::image(QSize size, Qt::AspectRatioMode mode) const
{
QPixmap cached_image;
if (PixmapCache::instance().find(m_pack_image_cache_key.key, &cached_image)) {
if (size.isNull())
return cached_image;
- return cached_image.scaled(size);
+ return cached_image.scaled(size, mode);
}
// No valid image we can get
- if (!m_pack_image_cache_key.was_ever_used)
+ if (!m_pack_image_cache_key.was_ever_used) {
return {};
+ } else {
+ qDebug() << "Resource Pack" << name() << "Had it's image evicted from the cache. reloading...";
+ PixmapCache::markCacheMissByEviciton();
+ }
// Imaged got evicted from the cache. Re-process it and retry.
- ResourcePackUtils::process(*this);
+ ResourcePackUtils::processPackPNG(*this);
return image(size);
}
@@ -94,6 +102,7 @@ std::pair<int, bool> ResourcePack::compare(const Resource& other, SortType type)
auto res = Resource::compare(other, type);
if (res.first != 0)
return res;
+ break;
}
case SortType::PACK_FORMAT: {
auto this_ver = packFormat();
@@ -103,6 +112,7 @@ std::pair<int, bool> ResourcePack::compare(const Resource& other, SortType type)
return { 1, type == SortType::PACK_FORMAT };
if (this_ver < other_ver)
return { -1, type == SortType::PACK_FORMAT };
+ break;
}
}
return { 0, false };
diff --git a/launcher/minecraft/mod/ResourcePack.h b/launcher/minecraft/mod/ResourcePack.h
index 7cb414d8..da354bc1 100644
--- a/launcher/minecraft/mod/ResourcePack.h
+++ b/launcher/minecraft/mod/ResourcePack.h
@@ -31,7 +31,7 @@ class ResourcePack : public Resource {
[[nodiscard]] QString description() const { return m_description; }
/** Gets the image of the resource pack, converted to a QPixmap for drawing, and scaled to size. */
- [[nodiscard]] QPixmap image(QSize size);
+ [[nodiscard]] QPixmap image(QSize size, Qt::AspectRatioMode mode = Qt::AspectRatioMode::IgnoreAspectRatio) const;
/** Thread-safe. */
void setPackFormat(int new_format_id);
@@ -40,7 +40,7 @@ class ResourcePack : public Resource {
void setDescription(QString new_description);
/** Thread-safe. */
- void setImage(QImage new_image);
+ void setImage(QImage new_image) const;
bool valid() const override;
@@ -67,5 +67,5 @@ class ResourcePack : public Resource {
struct {
QPixmapCache::Key key;
bool was_ever_used = false;
- } m_pack_image_cache_key;
+ } mutable m_pack_image_cache_key;
};
diff --git a/launcher/minecraft/mod/ResourcePackFolderModel.cpp b/launcher/minecraft/mod/ResourcePackFolderModel.cpp
index da4bd091..41455599 100644
--- a/launcher/minecraft/mod/ResourcePackFolderModel.cpp
+++ b/launcher/minecraft/mod/ResourcePackFolderModel.cpp
@@ -35,15 +35,26 @@
*/
#include "ResourcePackFolderModel.h"
+#include <qnamespace.h>
+#include <qsize.h>
+#include <QIcon>
+#include <QStyle>
+
+#include "Application.h"
#include "Version.h"
#include "minecraft/mod/tasks/BasicFolderLoadTask.h"
#include "minecraft/mod/tasks/LocalResourcePackParseTask.h"
-ResourcePackFolderModel::ResourcePackFolderModel(const QString& dir) : ResourceFolderModel(QDir(dir))
+ResourcePackFolderModel::ResourcePackFolderModel(const QString& dir, BaseInstance* instance)
+ : ResourceFolderModel(QDir(dir), instance)
{
- m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::PACK_FORMAT, SortType::DATE };
+ m_column_names = QStringList({ "Enable", "Image", "Name", "Pack Format", "Last Modified" });
+ m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Pack Format"), tr("Last Modified") });
+ m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::PACK_FORMAT, SortType::DATE};
+ m_column_resize_modes = { QHeaderView::ResizeToContents, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::ResizeToContents, QHeaderView::ResizeToContents };
+
}
QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const
@@ -78,12 +89,31 @@ QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const
default:
return {};
}
-
+ case Qt::DecorationRole: {
+ if (column == NameColumn && (at(row)->isSymLinkUnder(instDirPath()) || at(row)->isMoreThanOneHardLink()))
+ return APPLICATION->getThemedIcon("status-yellow");
+ if (column == ImageColumn) {
+ return at(row)->image({32, 32}, Qt::AspectRatioMode::KeepAspectRatioByExpanding);
+ }
+ return {};
+ }
case Qt::ToolTipRole: {
if (column == PackFormatColumn) {
//: The string being explained by this is in the format: ID (Lower version - Upper version)
return tr("The resource pack format ID, as well as the Minecraft versions it was designed for.");
}
+ if (column == NameColumn) {
+ if (at(row)->isSymLinkUnder(instDirPath())) {
+ return m_resources[row]->internal_id() +
+ tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original."
+ "\nCanonical Path: %1")
+ .arg(at(row)->fileinfo().canonicalFilePath());;
+ }
+ if (at(row)->isMoreThanOneHardLink()) {
+ return m_resources[row]->internal_id() +
+ tr("\nWarning: This resource is hard linked elsewhere. Editing it will also change the original.");
+ }
+ }
return m_resources[row]->internal_id();
}
case Qt::CheckStateRole:
@@ -104,13 +134,11 @@ QVariant ResourcePackFolderModel::headerData(int section, Qt::Orientation orient
case Qt::DisplayRole:
switch (section) {
case ActiveColumn:
- return QString();
case NameColumn:
- return tr("Name");
case PackFormatColumn:
- return tr("Pack Format");
case DateColumn:
- return tr("Last changed");
+ case ImageColumn:
+ return columnNames().at(section);
default:
return {};
}
@@ -129,6 +157,11 @@ QVariant ResourcePackFolderModel::headerData(int section, Qt::Orientation orient
default:
return {};
}
+ case Qt::SizeHintRole:
+ if (section == ImageColumn) {
+ return QSize(64,0);
+ }
+ return {};
default:
return {};
}
diff --git a/launcher/minecraft/mod/ResourcePackFolderModel.h b/launcher/minecraft/mod/ResourcePackFolderModel.h
index cb620ce2..531d8192 100644
--- a/launcher/minecraft/mod/ResourcePackFolderModel.h
+++ b/launcher/minecraft/mod/ResourcePackFolderModel.h
@@ -11,13 +11,16 @@ public:
enum Columns
{
ActiveColumn = 0,
+ ImageColumn,
NameColumn,
PackFormatColumn,
DateColumn,
NUM_COLUMNS
};
- explicit ResourcePackFolderModel(const QString &dir);
+ explicit ResourcePackFolderModel(const QString &dir, BaseInstance* instance);
+
+ virtual QString id() const override { return "resourcepacks"; }
[[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
diff --git a/launcher/minecraft/mod/ShaderPackFolderModel.h b/launcher/minecraft/mod/ShaderPackFolderModel.h
index a3aa958f..f8249962 100644
--- a/launcher/minecraft/mod/ShaderPackFolderModel.h
+++ b/launcher/minecraft/mod/ShaderPackFolderModel.h
@@ -6,5 +6,9 @@ class ShaderPackFolderModel : public ResourceFolderModel {
Q_OBJECT
public:
- explicit ShaderPackFolderModel(const QString& dir) : ResourceFolderModel(QDir(dir)) {}
+ explicit ShaderPackFolderModel(const QString& dir, BaseInstance* instance)
+ : ResourceFolderModel(QDir(dir), instance)
+ {}
+
+ virtual QString id() const override { return "shaderpacks"; }
};
diff --git a/launcher/minecraft/mod/TexturePack.cpp b/launcher/minecraft/mod/TexturePack.cpp
index 99d55584..c7a50a97 100644
--- a/launcher/minecraft/mod/TexturePack.cpp
+++ b/launcher/minecraft/mod/TexturePack.cpp
@@ -23,6 +23,8 @@
#include <QMap>
#include <QRegularExpression>
+#include "MTPixmapCache.h"
+
#include "minecraft/mod/tasks/LocalTexturePackParseTask.h"
void TexturePack::setDescription(QString new_description)
@@ -32,34 +34,41 @@ void TexturePack::setDescription(QString new_description)
m_description = new_description;
}
-void TexturePack::setImage(QImage new_image)
+void TexturePack::setImage(QImage new_image) const
{
QMutexLocker locker(&m_data_lock);
Q_ASSERT(!new_image.isNull());
if (m_pack_image_cache_key.key.isValid())
- QPixmapCache::remove(m_pack_image_cache_key.key);
+ PixmapCache::remove(m_pack_image_cache_key.key);
+
+ // scale the image to avoid flooding the pixmapcache
+ auto pixmap = QPixmap::fromImage(new_image.scaled({64, 64}, Qt::AspectRatioMode::KeepAspectRatioByExpanding));
- m_pack_image_cache_key.key = QPixmapCache::insert(QPixmap::fromImage(new_image));
+ m_pack_image_cache_key.key = PixmapCache::insert(pixmap);
m_pack_image_cache_key.was_ever_used = true;
}
-QPixmap TexturePack::image(QSize size)
+QPixmap TexturePack::image(QSize size, Qt::AspectRatioMode mode) const
{
QPixmap cached_image;
- if (QPixmapCache::find(m_pack_image_cache_key.key, &cached_image)) {
+ if (PixmapCache::find(m_pack_image_cache_key.key, &cached_image)) {
if (size.isNull())
return cached_image;
- return cached_image.scaled(size);
+ return cached_image.scaled(size, mode);
}
// No valid image we can get
- if (!m_pack_image_cache_key.was_ever_used)
+ if (!m_pack_image_cache_key.was_ever_used) {
return {};
+ } else {
+ qDebug() << "Texture Pack" << name() << "Had it's image evicted from the cache. reloading...";
+ PixmapCache::markCacheMissByEviciton();
+ }
// Imaged got evicted from the cache. Re-process it and retry.
- TexturePackUtils::process(*this);
+ TexturePackUtils::processPackPNG(*this);
return image(size);
}
diff --git a/launcher/minecraft/mod/TexturePack.h b/launcher/minecraft/mod/TexturePack.h
index 81bd5c69..57700565 100644
--- a/launcher/minecraft/mod/TexturePack.h
+++ b/launcher/minecraft/mod/TexturePack.h
@@ -40,13 +40,13 @@ class TexturePack : public Resource {
[[nodiscard]] QString description() const { return m_description; }
/** Gets the image of the texture pack, converted to a QPixmap for drawing, and scaled to size. */
- [[nodiscard]] QPixmap image(QSize size);
+ [[nodiscard]] QPixmap image(QSize size, Qt::AspectRatioMode mode = Qt::AspectRatioMode::IgnoreAspectRatio) const;
/** Thread-safe. */
void setDescription(QString new_description);
/** Thread-safe. */
- void setImage(QImage new_image);
+ void setImage(QImage new_image) const;
bool valid() const override;
@@ -65,5 +65,5 @@ class TexturePack : public Resource {
struct {
QPixmapCache::Key key;
bool was_ever_used = false;
- } m_pack_image_cache_key;
+ } mutable m_pack_image_cache_key;
};
diff --git a/launcher/minecraft/mod/TexturePackFolderModel.cpp b/launcher/minecraft/mod/TexturePackFolderModel.cpp
index 5a32cfaf..531a7023 100644
--- a/launcher/minecraft/mod/TexturePackFolderModel.cpp
+++ b/launcher/minecraft/mod/TexturePackFolderModel.cpp
@@ -33,13 +33,24 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+#include <QCoreApplication>
+
+#include "Application.h"
#include "TexturePackFolderModel.h"
#include "minecraft/mod/tasks/BasicFolderLoadTask.h"
#include "minecraft/mod/tasks/LocalTexturePackParseTask.h"
-TexturePackFolderModel::TexturePackFolderModel(const QString &dir) : ResourceFolderModel(QDir(dir)) {}
+TexturePackFolderModel::TexturePackFolderModel(const QString& dir, BaseInstance* instance)
+ : ResourceFolderModel(QDir(dir), instance)
+{
+ m_column_names = QStringList({ "Enable", "Image", "Name", "Last Modified" });
+ m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Last Modified") });
+ m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::DATE };
+ m_column_resize_modes = { QHeaderView::ResizeToContents, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::ResizeToContents};
+
+}
Task* TexturePackFolderModel::createUpdateTask()
{
@@ -50,3 +61,96 @@ Task* TexturePackFolderModel::createParseTask(Resource& resource)
{
return new LocalTexturePackParseTask(m_next_resolution_ticket, static_cast<TexturePack&>(resource));
}
+
+
+QVariant TexturePackFolderModel::data(const QModelIndex& index, int role) const
+{
+ if (!validateIndex(index))
+ return {};
+
+ int row = index.row();
+ int column = index.column();
+
+ switch (role) {
+ case Qt::DisplayRole:
+ switch (column) {
+ case NameColumn:
+ return m_resources[row]->name();
+ case DateColumn:
+ return m_resources[row]->dateTimeChanged();
+ default:
+ return {};
+ }
+ case Qt::ToolTipRole:
+ if (column == NameColumn) {
+ if (at(row)->isSymLinkUnder(instDirPath())) {
+ return m_resources[row]->internal_id() +
+ tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original."
+ "\nCanonical Path: %1")
+ .arg(at(row)->fileinfo().canonicalFilePath());;
+ }
+ if (at(row)->isMoreThanOneHardLink()) {
+ return m_resources[row]->internal_id() +
+ tr("\nWarning: This resource is hard linked elsewhere. Editing it will also change the original.");
+ }
+ }
+
+ return m_resources[row]->internal_id();
+ case Qt::DecorationRole: {
+ if (column == NameColumn && (at(row)->isSymLinkUnder(instDirPath()) || at(row)->isMoreThanOneHardLink()))
+ return APPLICATION->getThemedIcon("status-yellow");
+ if (column == ImageColumn) {
+ return at(row)->image({32, 32}, Qt::AspectRatioMode::KeepAspectRatioByExpanding);
+ }
+ return {};
+ }
+ case Qt::CheckStateRole:
+ if (column == ActiveColumn) {
+ return m_resources[row]->enabled() ? Qt::Checked : Qt::Unchecked;
+ }
+ return {};
+ default:
+ return {};
+ }
+}
+
+QVariant TexturePackFolderModel::headerData(int section, Qt::Orientation orientation, int role) const
+{
+ switch (role) {
+ case Qt::DisplayRole:
+ switch (section) {
+ case ActiveColumn:
+ case NameColumn:
+ case DateColumn:
+ case ImageColumn:
+ return columnNames().at(section);
+ default:
+ return {};
+ }
+ case Qt::ToolTipRole: {
+ switch (section) {
+ case ActiveColumn:
+ //: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc.
+ return tr("Is the resource enabled?");
+ case NameColumn:
+ //: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc.
+ return tr("The name of the resource.");
+ case DateColumn:
+ //: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc.
+ return tr("The date and time this resource was last changed (or added).");
+ default:
+ return {};
+ }
+ }
+ default:
+ break;
+ }
+
+ return {};
+}
+
+int TexturePackFolderModel::columnCount(const QModelIndex& parent) const
+{
+ return parent.isValid() ? 0 : NUM_COLUMNS;
+}
+
diff --git a/launcher/minecraft/mod/TexturePackFolderModel.h b/launcher/minecraft/mod/TexturePackFolderModel.h
index 261f83b4..71a8bdd1 100644
--- a/launcher/minecraft/mod/TexturePackFolderModel.h
+++ b/launcher/minecraft/mod/TexturePackFolderModel.h
@@ -38,12 +38,35 @@
#include "ResourceFolderModel.h"
+#include "TexturePack.h"
+
class TexturePackFolderModel : public ResourceFolderModel
{
Q_OBJECT
public:
- explicit TexturePackFolderModel(const QString &dir);
+
+ enum Columns
+ {
+ ActiveColumn = 0,
+ ImageColumn,
+ NameColumn,
+ DateColumn,
+ NUM_COLUMNS
+ };
+
+ explicit TexturePackFolderModel(const QString &dir, std::shared_ptr<const BaseInstance> instance);
+
+ virtual QString id() const override { return "texturepacks"; }
+
+ [[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
+
+ [[nodiscard]] QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
+ [[nodiscard]] int columnCount(const QModelIndex &parent) const override;
+
+ explicit TexturePackFolderModel(const QString &dir, BaseInstance* instance);
[[nodiscard]] Task* createUpdateTask() override;
[[nodiscard]] Task* createParseTask(Resource&) override;
+
+ RESOURCE_HELPERS(TexturePack)
};
diff --git a/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp b/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp
new file mode 100644
index 00000000..f8ecdb33
--- /dev/null
+++ b/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp
@@ -0,0 +1,252 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "GetModDependenciesTask.h"
+
+#include <QDebug>
+#include <algorithm>
+#include <memory>
+#include "Json.h"
+#include "QObjectPtr.h"
+#include "minecraft/mod/MetadataHandler.h"
+#include "modplatform/ModIndex.h"
+#include "modplatform/ResourceAPI.h"
+#include "modplatform/flame/FlameAPI.h"
+#include "modplatform/modrinth/ModrinthAPI.h"
+#include "tasks/ConcurrentTask.h"
+#include "tasks/SequentialTask.h"
+#include "ui/pages/modplatform/ModModel.h"
+#include "ui/pages/modplatform/flame/FlameResourceModels.h"
+#include "ui/pages/modplatform/modrinth/ModrinthResourceModels.h"
+
+static Version mcVersion(BaseInstance* inst)
+{
+ return static_cast<MinecraftInstance*>(inst)->getPackProfile()->getComponent("net.minecraft")->getVersion();
+}
+
+static ResourceAPI::ModLoaderTypes mcLoaders(BaseInstance* inst)
+{
+ return static_cast<MinecraftInstance*>(inst)->getPackProfile()->getModLoaders().value();
+}
+
+GetModDependenciesTask::GetModDependenciesTask(QObject* parent,
+ BaseInstance* instance,
+ ModFolderModel* folder,
+ QList<std::shared_ptr<PackDependency>> selected)
+ : SequentialTask(parent, tr("Get dependencies"))
+ , m_selected(selected)
+ , m_flame_provider{ ModPlatform::ResourceProvider::FLAME, std::make_shared<ResourceDownload::FlameModModel>(*instance),
+ std::make_shared<FlameAPI>() }
+ , m_modrinth_provider{ ModPlatform::ResourceProvider::MODRINTH, std::make_shared<ResourceDownload::ModrinthModModel>(*instance),
+ std::make_shared<ModrinthAPI>() }
+ , m_version(mcVersion(instance))
+ , m_loaderType(mcLoaders(instance))
+{
+ for (auto mod : folder->allMods())
+ if (auto meta = mod->metadata(); meta)
+ m_mods.append(meta);
+ prepare();
+};
+
+void GetModDependenciesTask::prepare()
+{
+ for (auto sel : m_selected) {
+ for (auto dep : getDependenciesForVersion(sel->version, sel->pack->provider)) {
+ addTask(prepareDependencyTask(dep, sel->pack->provider, 20));
+ }
+ }
+}
+
+ModPlatform::Dependency GetModDependenciesTask::getOverride(const ModPlatform::Dependency& dep,
+ const ModPlatform::ResourceProvider providerName)
+{
+ if (auto isQuilt = m_loaderType & ResourceAPI::Quilt; isQuilt || m_loaderType & ResourceAPI::Fabric) {
+ auto overide = ModPlatform::getOverrideDeps();
+ auto over = std::find_if(overide.cbegin(), overide.cend(), [dep, providerName, isQuilt](auto o) {
+ return o.provider == providerName && dep.addonId == (isQuilt ? o.fabric : o.quilt);
+ });
+ if (over != overide.cend()) {
+ return { isQuilt ? over->quilt : over->fabric, dep.type };
+ }
+ }
+ return dep;
+}
+
+QList<ModPlatform::Dependency> GetModDependenciesTask::getDependenciesForVersion(const ModPlatform::IndexedVersion& version,
+ const ModPlatform::ResourceProvider providerName)
+{
+ QList<ModPlatform::Dependency> c_dependencies;
+ for (auto ver_dep : version.dependencies) {
+ if (ver_dep.type != ModPlatform::DependencyType::REQUIRED)
+ continue;
+
+ auto isOnlyVersion = providerName == ModPlatform::ResourceProvider::MODRINTH && ver_dep.addonId.toString().isEmpty();
+ if (auto dep = std::find_if(c_dependencies.begin(), c_dependencies.end(),
+ [&ver_dep, isOnlyVersion](const ModPlatform::Dependency& i) {
+ return isOnlyVersion ? i.version == ver_dep.version : i.addonId == ver_dep.addonId;
+ });
+ dep != c_dependencies.end())
+ continue; // check the current dependency list
+
+ if (auto dep = std::find_if(m_selected.begin(), m_selected.end(),
+ [&ver_dep, providerName, isOnlyVersion](std::shared_ptr<PackDependency> i) {
+ return i->pack->provider == providerName && (isOnlyVersion ? i->version.version == ver_dep.version
+ : i->pack->addonId == ver_dep.addonId);
+ });
+ dep != m_selected.end())
+ continue; // check the selected versions
+
+ if (auto dep = std::find_if(m_mods.begin(), m_mods.end(),
+ [&ver_dep, providerName, isOnlyVersion](std::shared_ptr<Metadata::ModStruct> i) {
+ return i->provider == providerName &&
+ (isOnlyVersion ? i->file_id == ver_dep.version : i->project_id == ver_dep.addonId);
+ });
+ dep != m_mods.end())
+ continue; // check the existing mods
+
+ if (auto dep = std::find_if(m_pack_dependencies.begin(), m_pack_dependencies.end(),
+ [&ver_dep, providerName, isOnlyVersion](std::shared_ptr<PackDependency> i) {
+ return i->pack->provider == providerName && (isOnlyVersion ? i->version.version == ver_dep.addonId
+ : i->pack->addonId == ver_dep.addonId);
+ });
+ dep != m_pack_dependencies.end()) // check loaded dependencies
+ continue;
+
+ c_dependencies.append(getOverride(ver_dep, providerName));
+ }
+ return c_dependencies;
+};
+
+Task::Ptr GetModDependenciesTask::getProjectInfoTask(std::shared_ptr<PackDependency> pDep)
+{
+ auto provider = pDep->pack->provider == m_flame_provider.name ? m_flame_provider : m_modrinth_provider;
+ auto responseInfo = std::make_shared<QByteArray>();
+ auto info = provider.api->getProject(pDep->pack->addonId.toString(), responseInfo);
+ QObject::connect(info.get(), &NetJob::succeeded, [responseInfo, provider, pDep] {
+ QJsonParseError parse_error{};
+ QJsonDocument doc = QJsonDocument::fromJson(*responseInfo, &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();
+ qDebug() << *responseInfo;
+ return;
+ }
+ try {
+ auto obj = provider.name == ModPlatform::ResourceProvider::FLAME ? Json::requireObject(Json::requireObject(doc), "data")
+ : Json::requireObject(doc);
+ provider.mod->loadIndexedPack(*pDep->pack, obj);
+ } catch (const JSONValidationError& e) {
+ qDebug() << doc;
+ qWarning() << "Error while reading mod info: " << e.cause();
+ }
+ });
+ return info;
+}
+
+Task::Ptr GetModDependenciesTask::prepareDependencyTask(const ModPlatform::Dependency& dep,
+ const ModPlatform::ResourceProvider providerName,
+ int level)
+{
+ auto pDep = std::make_shared<PackDependency>();
+ pDep->dependency = dep;
+ pDep->pack = std::make_shared<ModPlatform::IndexedPack>();
+ pDep->pack->addonId = dep.addonId;
+ pDep->pack->provider = providerName;
+
+ m_pack_dependencies.append(pDep);
+ auto provider = providerName == m_flame_provider.name ? m_flame_provider : m_modrinth_provider;
+
+ auto tasks = makeShared<SequentialTask>(
+ this, QString("DependencyInfo: %1").arg(dep.addonId.toString().isEmpty() ? dep.version : dep.addonId.toString()));
+
+ if (!dep.addonId.toString().isEmpty()) {
+ tasks->addTask(getProjectInfoTask(pDep));
+ }
+
+ ResourceAPI::DependencySearchArgs args = { dep, m_version, m_loaderType };
+ ResourceAPI::DependencySearchCallbacks callbacks;
+
+ callbacks.on_succeed = [dep, provider, pDep, level, this](auto& doc, auto& pack) {
+ try {
+ QJsonArray arr;
+ if (dep.version.length() != 0 && doc.isObject()) {
+ arr.append(doc.object());
+ } else {
+ arr = doc.isObject() ? Json::ensureArray(doc.object(), "data") : doc.array();
+ }
+ pDep->version = provider.mod->loadDependencyVersions(dep, arr);
+ if (!pDep->version.addonId.isValid()) {
+ if (m_loaderType & ResourceAPI::Quilt) { // falback for quilt
+ auto overide = ModPlatform::getOverrideDeps();
+ auto over = std::find_if(overide.cbegin(), overide.cend(),
+ [dep, provider](auto o) { return o.provider == provider.name && dep.addonId == o.quilt; });
+ if (over != overide.cend()) {
+ removePack(dep.addonId);
+ addTask(prepareDependencyTask({ over->fabric, dep.type }, provider.name, level));
+ return;
+ }
+ }
+ qWarning() << "Error while reading mod version empty ";
+ qDebug() << doc;
+ return;
+ }
+ pDep->version.is_currently_selected = true;
+ pDep->pack->versions = { pDep->version };
+ pDep->pack->versionsLoaded = true;
+
+ } catch (const JSONValidationError& e) {
+ qDebug() << doc;
+ qWarning() << "Error while reading mod version: " << e.cause();
+ return;
+ }
+ if (level == 0) {
+ qWarning() << "Dependency cycle exeeded";
+ return;
+ }
+ if (dep.addonId.toString().isEmpty() && !pDep->version.addonId.toString().isEmpty()) {
+ pDep->pack->addonId = pDep->version.addonId;
+ auto dep = getOverride({ pDep->version.addonId, pDep->dependency.type }, provider.name);
+ if (dep.addonId != pDep->version.addonId) {
+ removePack(pDep->version.addonId);
+ addTask(prepareDependencyTask(dep, provider.name, level));
+ } else
+ addTask(getProjectInfoTask(pDep));
+ }
+ for (auto dep : getDependenciesForVersion(pDep->version, provider.name)) {
+ addTask(prepareDependencyTask(dep, provider.name, level - 1));
+ }
+ };
+
+ auto version = provider.api->getDependencyVersion(std::move(args), std::move(callbacks));
+ tasks->addTask(version);
+ return tasks;
+};
+
+void GetModDependenciesTask::removePack(const QVariant addonId)
+{
+ auto pred = [addonId](const std::shared_ptr<PackDependency>& v) { return v->pack->addonId == addonId; };
+#if QT_VERSION >= QT_VERSION_CHECK(6, 1, 0)
+ m_pack_dependencies.removeIf(pred);
+#else
+ for (auto it = m_pack_dependencies.begin(); it != m_pack_dependencies.end();)
+ if (pred(*it))
+ it = m_pack_dependencies.erase(it);
+ else
+ ++it;
+#endif
+}
diff --git a/launcher/minecraft/mod/tasks/GetModDependenciesTask.h b/launcher/minecraft/mod/tasks/GetModDependenciesTask.h
new file mode 100644
index 00000000..50eba6af
--- /dev/null
+++ b/launcher/minecraft/mod/tasks/GetModDependenciesTask.h
@@ -0,0 +1,84 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <QDir>
+#include <QEventLoop>
+#include <QList>
+#include <QVariant>
+#include <functional>
+#include <memory>
+
+#include "minecraft/mod/MetadataHandler.h"
+#include "minecraft/mod/ModFolderModel.h"
+#include "modplatform/ModIndex.h"
+#include "modplatform/ResourceAPI.h"
+#include "tasks/SequentialTask.h"
+#include "tasks/Task.h"
+#include "ui/pages/modplatform/ModModel.h"
+
+class GetModDependenciesTask : public SequentialTask {
+ Q_OBJECT
+ public:
+ using Ptr = shared_qobject_ptr<GetModDependenciesTask>;
+
+ struct PackDependency {
+ ModPlatform::Dependency dependency;
+ ModPlatform::IndexedPack::Ptr pack;
+ ModPlatform::IndexedVersion version;
+ PackDependency() = default;
+ PackDependency(const ModPlatform::IndexedPack::Ptr p, const ModPlatform::IndexedVersion& v)
+ {
+ pack = p;
+ version = v;
+ }
+ };
+
+ struct Provider {
+ ModPlatform::ResourceProvider name;
+ std::shared_ptr<ResourceDownload::ModModel> mod;
+ std::shared_ptr<ResourceAPI> api;
+ };
+
+ explicit GetModDependenciesTask(QObject* parent,
+ BaseInstance* instance,
+ ModFolderModel* folder,
+ QList<std::shared_ptr<PackDependency>> selected);
+
+ auto getDependecies() const -> QList<std::shared_ptr<PackDependency>> { return m_pack_dependencies; }
+
+ protected slots:
+ Task::Ptr prepareDependencyTask(const ModPlatform::Dependency&, const ModPlatform::ResourceProvider, int);
+ QList<ModPlatform::Dependency> getDependenciesForVersion(const ModPlatform::IndexedVersion&,
+ const ModPlatform::ResourceProvider providerName);
+ void prepare();
+ Task::Ptr getProjectInfoTask(std::shared_ptr<PackDependency> pDep);
+ ModPlatform::Dependency getOverride(const ModPlatform::Dependency&, const ModPlatform::ResourceProvider providerName);
+ void removePack(const QVariant addonId);
+
+ private:
+ QList<std::shared_ptr<PackDependency>> m_pack_dependencies;
+ QList<std::shared_ptr<Metadata::ModStruct>> m_mods;
+ QList<std::shared_ptr<PackDependency>> m_selected;
+ Provider m_flame_provider;
+ Provider m_modrinth_provider;
+
+ Version m_version;
+ ResourceAPI::ModLoaderTypes m_loaderType;
+};
diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp
index da27a505..264019f8 100644
--- a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp
+++ b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp
@@ -52,6 +52,10 @@ ModDetails ReadMCModInfo(QByteArray contents)
authors = firstObj.value("authors").toArray();
}
+ if (firstObj.contains("logoFile")) {
+ details.icon_file = firstObj.value("logoFile").toString();
+ }
+
for (auto author : authors) {
details.authors.append(author.toString());
}
@@ -166,6 +170,31 @@ ModDetails ReadMCModTOML(QByteArray contents)
}
details.homeurl = homeurl;
+ QString issueTrackerURL = "";
+ if (auto issueTrackerURLDatum = tomlData["issueTrackerURL"].as_string()) {
+ issueTrackerURL = QString::fromStdString(issueTrackerURLDatum->get());
+ } else if (auto issueTrackerURLDatum = (*modsTable)["issueTrackerURL"].as_string()) {
+ issueTrackerURL = QString::fromStdString(issueTrackerURLDatum->get());
+ }
+ details.issue_tracker = issueTrackerURL;
+
+ QString license = "";
+ if (auto licenseDatum = tomlData["license"].as_string()) {
+ license = QString::fromStdString(licenseDatum->get());
+ } else if (auto licenseDatum =(*modsTable)["license"].as_string()) {
+ license = QString::fromStdString(licenseDatum->get());
+ }
+ if (!license.isEmpty())
+ details.licenses.append(ModLicense(license));
+
+ QString logoFile = "";
+ if (auto logoFileDatum = tomlData["logoFile"].as_string()) {
+ logoFile = QString::fromStdString(logoFileDatum->get());
+ } else if (auto logoFileDatum =(*modsTable)["logoFile"].as_string()) {
+ logoFile = QString::fromStdString(logoFileDatum->get());
+ }
+ details.icon_file = logoFile;
+
return details;
}
@@ -201,6 +230,57 @@ ModDetails ReadFabricModInfo(QByteArray contents)
if (contact.contains("homepage")) {
details.homeurl = contact.value("homepage").toString();
}
+ if (contact.contains("issues")) {
+ details.issue_tracker = contact.value("issues").toString();
+ }
+ }
+
+ if (object.contains("license")) {
+ auto license = object.value("license");
+ if (license.isArray()) {
+ for (auto l : license.toArray()) {
+ if (l.isString()) {
+ details.licenses.append(ModLicense(l.toString()));
+ } else if (l.isObject()) {
+ auto obj = l.toObject();
+ details.licenses.append(ModLicense(obj.value("name").toString(), obj.value("id").toString(),
+ obj.value("url").toString(), obj.value("description").toString()));
+ }
+ }
+ } else if (license.isString()) {
+ details.licenses.append(ModLicense(license.toString()));
+ } else if (license.isObject()) {
+ auto obj = license.toObject();
+ details.licenses.append(ModLicense(obj.value("name").toString(), obj.value("id").toString(), obj.value("url").toString(),
+ obj.value("description").toString()));
+ }
+ }
+
+ if (object.contains("icon")) {
+ auto icon = object.value("icon");
+ if (icon.isObject()) {
+ auto obj = icon.toObject();
+ // take the largest icon
+ int largest = 0;
+ for (auto key : obj.keys()) {
+ auto size = key.split('x').first().toInt();
+ if (size > largest) {
+ largest = size;
+ }
+ }
+ if (largest > 0) {
+ auto key = QString::number(largest) + "x" + QString::number(largest);
+ details.icon_file = obj.value(key).toString();
+ } else { // parsing the sizes failed
+ // take the first
+ for (auto i : obj) {
+ details.icon_file = i.toString();
+ break;
+ }
+ }
+ } else if (icon.isString()) {
+ details.icon_file = icon.toString();
+ }
}
}
return details;
@@ -238,11 +318,63 @@ ModDetails ReadQuiltModInfo(QByteArray contents)
if (modContact.contains("homepage")) {
details.homeurl = Json::requireString(modContact.value("homepage"));
}
+ if (modContact.contains("issues")) {
+ details.issue_tracker = Json::requireString(modContact.value("issues"));
+ }
+
+ if (modMetadata.contains("license")) {
+ auto license = modMetadata.value("license");
+ if (license.isArray()) {
+ for (auto l : license.toArray()) {
+ if (l.isString()) {
+ details.licenses.append(ModLicense(l.toString()));
+ } else if (l.isObject()) {
+ auto obj = l.toObject();
+ details.licenses.append(ModLicense(obj.value("name").toString(), obj.value("id").toString(),
+ obj.value("url").toString(), obj.value("description").toString()));
+ }
+ }
+ } else if (license.isString()) {
+ details.licenses.append(ModLicense(license.toString()));
+ } else if (license.isObject()) {
+ auto obj = license.toObject();
+ details.licenses.append(ModLicense(obj.value("name").toString(), obj.value("id").toString(), obj.value("url").toString(),
+ obj.value("description").toString()));
+ }
+ }
+
+ if (modMetadata.contains("icon")) {
+ auto icon = modMetadata.value("icon");
+ if (icon.isObject()) {
+ auto obj = icon.toObject();
+ // take the largest icon
+ int largest = 0;
+ for (auto key : obj.keys()) {
+ auto size = key.split('x').first().toInt();
+ if (size > largest) {
+ largest = size;
+ }
+ }
+ if (largest > 0) {
+ auto key = QString::number(largest) + "x" + QString::number(largest);
+ details.icon_file = obj.value(key).toString();
+ } else { // parsing the sizes failed
+ // take the first
+ for (auto i : obj) {
+ details.icon_file = i.toString();
+ break;
+ }
+ }
+ } else if (icon.isString()) {
+ details.icon_file = icon.toString();
+ }
+ }
+
}
return details;
}
-ModDetails ReadForgeInfo(QByteArray contents)
+ModDetails ReadForgeInfo(QString fileName)
{
ModDetails details;
// Read the data
@@ -250,7 +382,7 @@ ModDetails ReadForgeInfo(QByteArray contents)
details.mod_id = "Forge";
details.homeurl = "http://www.minecraftforge.net/forum/";
INIFile ini;
- if (!ini.loadFile(contents))
+ if (!ini.loadFile(fileName))
return details;
QString major = ini.get("forge.major.number", "0").toString();
@@ -422,7 +554,7 @@ bool processZIP(Mod& mod, ProcessingLevel level)
return false;
}
- details = ReadForgeInfo(file.readAll());
+ details = ReadForgeInfo(file.getFileName());
file.close();
zip.close();
@@ -515,6 +647,85 @@ bool validate(QFileInfo file)
return ModUtils::process(mod, ProcessingLevel::BasicInfoOnly) && mod.valid();
}
+bool processIconPNG(const Mod& mod, QByteArray&& raw_data)
+{
+ auto img = QImage::fromData(raw_data);
+ if (!img.isNull()) {
+ mod.setIcon(img);
+ } else {
+ qWarning() << "Failed to parse mod logo:" << mod.iconPath() << "from" << mod.name();
+ return false;
+ }
+ return true;
+}
+
+bool loadIconFile(const Mod& mod) {
+ if (mod.iconPath().isEmpty()) {
+ qWarning() << "No Iconfile set, be sure to parse the mod first";
+ return false;
+ }
+
+ auto png_invalid = [&mod]() {
+ qWarning() << "Mod at" << mod.fileinfo().filePath() << "does not have a valid icon";
+ return false;
+ };
+
+ switch (mod.type()) {
+ case ResourceType::FOLDER:
+ {
+ QFileInfo icon_info(FS::PathCombine(mod.fileinfo().filePath(), mod.iconPath()));
+ if (icon_info.exists() && icon_info.isFile()) {
+ QFile icon(icon_info.filePath());
+ if (!icon.open(QIODevice::ReadOnly))
+ return false;
+ auto data = icon.readAll();
+
+ bool icon_result = ModUtils::processIconPNG(mod, std::move(data));
+
+ icon.close();
+
+ if (!icon_result) {
+ return png_invalid(); // icon invalid
+ }
+ }
+ }
+ case ResourceType::ZIPFILE:
+ {
+ QuaZip zip(mod.fileinfo().filePath());
+ if (!zip.open(QuaZip::mdUnzip))
+ return false;
+
+ QuaZipFile file(&zip);
+
+ if (zip.setCurrentFile(mod.iconPath())) {
+ if (!file.open(QIODevice::ReadOnly)) {
+ qCritical() << "Failed to open file in zip.";
+ zip.close();
+ return png_invalid();
+ }
+
+ auto data = file.readAll();
+
+ bool icon_result = ModUtils::processIconPNG(mod, std::move(data));
+
+ file.close();
+ if (!icon_result) {
+ return png_invalid(); // icon png invalid
+ }
+ } else {
+ return png_invalid(); // could not set icon as current file.
+ }
+ }
+ case ResourceType::LITEMOD:
+ {
+ return false; // can lightmods even have icons?
+ }
+ default:
+ qWarning() << "Invalid type for mod, can not load icon.";
+ return false;
+ }
+}
+
} // namespace ModUtils
LocalModParseTask::LocalModParseTask(int token, ResourceType type, const QFileInfo& modFile)
diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.h b/launcher/minecraft/mod/tasks/LocalModParseTask.h
index 38dae135..a0321709 100644
--- a/launcher/minecraft/mod/tasks/LocalModParseTask.h
+++ b/launcher/minecraft/mod/tasks/LocalModParseTask.h
@@ -25,6 +25,9 @@ bool processLitemod(Mod& mod, ProcessingLevel level = ProcessingLevel::Full);
/** Checks whether a file is valid as a mod or not. */
bool validate(QFileInfo file);
+
+bool processIconPNG(const Mod& mod, QByteArray&& raw_data);
+bool loadIconFile(const Mod& mod);
} // namespace ModUtils
class LocalModParseTask : public Task {
diff --git a/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp b/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp
index cc4e252c..4352fad9 100644
--- a/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp
+++ b/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp
@@ -1,25 +1,24 @@
// 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/>.
-*/
+ * Prism Launcher - 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 "LocalModUpdateTask.h"
-#include "Application.h"
#include "FileSystem.h"
#include "minecraft/mod/MetadataHandler.h"
diff --git a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp
index 4bf0b80d..a67c56a8 100644
--- a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp
+++ b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp
@@ -165,15 +165,16 @@ bool processZIP(ResourcePack& pack, ProcessingLevel level)
bool pack_png_result = ResourcePackUtils::processPackPNG(pack, std::move(data));
file.close();
+ zip.close();
if (!pack_png_result) {
return png_invalid(); // pack.png invalid
}
} else {
+ zip.close();
return png_invalid(); // could not set pack.mcmeta as current file.
}
zip.close();
-
return true;
}
@@ -193,7 +194,7 @@ bool processMCMeta(ResourcePack& pack, QByteArray&& raw_data)
return true;
}
-bool processPackPNG(ResourcePack& pack, QByteArray&& raw_data)
+bool processPackPNG(const ResourcePack& pack, QByteArray&& raw_data)
{
auto img = QImage::fromData(raw_data);
if (!img.isNull()) {
@@ -205,6 +206,68 @@ bool processPackPNG(ResourcePack& pack, QByteArray&& raw_data)
return true;
}
+bool processPackPNG(const ResourcePack& pack)
+{
+ auto png_invalid = [&pack]() {
+ qWarning() << "Resource pack at" << pack.fileinfo().filePath() << "does not have a valid pack.png";
+ return false;
+ };
+
+ switch (pack.type()) {
+ case ResourceType::FOLDER:
+ {
+ QFileInfo image_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.png"));
+ if (image_file_info.exists() && image_file_info.isFile()) {
+ QFile pack_png_file(image_file_info.filePath());
+ if (!pack_png_file.open(QIODevice::ReadOnly))
+ return png_invalid(); // can't open pack.png file
+
+ auto data = pack_png_file.readAll();
+
+ bool pack_png_result = ResourcePackUtils::processPackPNG(pack, std::move(data));
+
+ pack_png_file.close();
+ if (!pack_png_result) {
+ return png_invalid(); // pack.png invalid
+ }
+ } else {
+ return png_invalid(); // pack.png does not exists or is not a valid file.
+ }
+ }
+ case ResourceType::ZIPFILE:
+ {
+ Q_ASSERT(pack.type() == ResourceType::ZIPFILE);
+
+ QuaZip zip(pack.fileinfo().filePath());
+ if (!zip.open(QuaZip::mdUnzip))
+ return false; // can't open zip file
+
+ QuaZipFile file(&zip);
+ if (zip.setCurrentFile("pack.png")) {
+ if (!file.open(QIODevice::ReadOnly)) {
+ qCritical() << "Failed to open file in zip.";
+ zip.close();
+ return png_invalid();
+ }
+
+ auto data = file.readAll();
+
+ bool pack_png_result = ResourcePackUtils::processPackPNG(pack, std::move(data));
+
+ file.close();
+ if (!pack_png_result) {
+ return png_invalid(); // pack.png invalid
+ }
+ } else {
+ return png_invalid(); // could not set pack.mcmeta as current file.
+ }
+ }
+ default:
+ qWarning() << "Invalid type for resource pack parse task!";
+ return false;
+ }
+}
+
bool validate(QFileInfo file)
{
ResourcePack rp{ file };
diff --git a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h
index d0c24c2b..58d90b3b 100644
--- a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h
+++ b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h
@@ -35,7 +35,10 @@ bool processZIP(ResourcePack& pack, ProcessingLevel level = ProcessingLevel::Ful
bool processFolder(ResourcePack& pack, ProcessingLevel level = ProcessingLevel::Full);
bool processMCMeta(ResourcePack& pack, QByteArray&& raw_data);
-bool processPackPNG(ResourcePack& pack, QByteArray&& raw_data);
+bool processPackPNG(const ResourcePack& pack, QByteArray&& raw_data);
+
+/// processes ONLY the pack.png (rest of the pack may be invalid)
+bool processPackPNG(const ResourcePack& pack);
/** Checks whether a file is valid as a resource pack or not. */
bool validate(QFileInfo file);
diff --git a/launcher/minecraft/mod/tasks/LocalResourceParse.cpp b/launcher/minecraft/mod/tasks/LocalResourceParse.cpp
index 4d760df2..0894049c 100644
--- a/launcher/minecraft/mod/tasks/LocalResourceParse.cpp
+++ b/launcher/minecraft/mod/tasks/LocalResourceParse.cpp
@@ -44,7 +44,11 @@ static const QMap<PackedResourceType, QString> s_packed_type_names = {
namespace ResourceUtils {
PackedResourceType identify(QFileInfo file){
if (file.exists() && file.isFile()) {
- if (ResourcePackUtils::validate(file)) {
+ if (ModUtils::validate(file)) {
+ // mods can contain resource and data packs so they must be tested first
+ qDebug() << file.fileName() << "is a mod";
+ return PackedResourceType::Mod;
+ } else if (ResourcePackUtils::validate(file)) {
qDebug() << file.fileName() << "is a resource pack";
return PackedResourceType::ResourcePack;
} else if (TexturePackUtils::validate(file)) {
@@ -53,9 +57,6 @@ PackedResourceType identify(QFileInfo file){
} else if (DataPackUtils::validate(file)) {
qDebug() << file.fileName() << "is a data pack";
return PackedResourceType::DataPack;
- } else if (ModUtils::validate(file)) {
- qDebug() << file.fileName() << "is a mod";
- return PackedResourceType::Mod;
} else if (WorldSaveUtils::validate(file)) {
qDebug() << file.fileName() << "is a world save";
return PackedResourceType::WorldSave;
diff --git a/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp
index 38f1d7c1..a72e8115 100644
--- a/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp
+++ b/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp
@@ -131,6 +131,7 @@ bool processZIP(TexturePack& pack, ProcessingLevel level)
bool packPNG_result = TexturePackUtils::processPackPNG(pack, std::move(data));
file.close();
+ zip.close();
if (!packPNG_result) {
return false;
}
@@ -147,7 +148,7 @@ bool processPackTXT(TexturePack& pack, QByteArray&& raw_data)
return true;
}
-bool processPackPNG(TexturePack& pack, QByteArray&& raw_data)
+bool processPackPNG(const TexturePack& pack, QByteArray&& raw_data)
{
auto img = QImage::fromData(raw_data);
if (!img.isNull()) {
@@ -159,6 +160,70 @@ bool processPackPNG(TexturePack& pack, QByteArray&& raw_data)
return true;
}
+bool processPackPNG(const TexturePack& pack)
+{
+ auto png_invalid = [&pack]() {
+ qWarning() << "Texture pack at" << pack.fileinfo().filePath() << "does not have a valid pack.png";
+ return false;
+ };
+
+ switch (pack.type()) {
+ case ResourceType::FOLDER:
+ {
+ QFileInfo image_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.png"));
+ if (image_file_info.exists() && image_file_info.isFile()) {
+ QFile pack_png_file(image_file_info.filePath());
+ if (!pack_png_file.open(QIODevice::ReadOnly))
+ return png_invalid(); // can't open pack.png file
+
+ auto data = pack_png_file.readAll();
+
+ bool pack_png_result = TexturePackUtils::processPackPNG(pack, std::move(data));
+
+ pack_png_file.close();
+ if (!pack_png_result) {
+ return png_invalid(); // pack.png invalid
+ }
+ } else {
+ return png_invalid(); // pack.png does not exists or is not a valid file.
+ }
+ }
+ case ResourceType::ZIPFILE:
+ {
+ Q_ASSERT(pack.type() == ResourceType::ZIPFILE);
+
+ QuaZip zip(pack.fileinfo().filePath());
+ if (!zip.open(QuaZip::mdUnzip))
+ return false; // can't open zip file
+
+ QuaZipFile file(&zip);
+ if (zip.setCurrentFile("pack.png")) {
+ if (!file.open(QIODevice::ReadOnly)) {
+ qCritical() << "Failed to open file in zip.";
+ zip.close();
+ return png_invalid();
+ }
+
+ auto data = file.readAll();
+
+ bool pack_png_result = TexturePackUtils::processPackPNG(pack, std::move(data));
+
+ file.close();
+ if (!pack_png_result) {
+ zip.close();
+ return png_invalid(); // pack.png invalid
+ }
+ } else {
+ zip.close();
+ return png_invalid(); // could not set pack.mcmeta as current file.
+ }
+ }
+ default:
+ qWarning() << "Invalid type for resource pack parse task!";
+ return false;
+ }
+}
+
bool validate(QFileInfo file)
{
TexturePack rp{ file };
diff --git a/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.h b/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.h
index 1589f8cb..6b91565a 100644
--- a/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.h
+++ b/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.h
@@ -36,7 +36,10 @@ bool processZIP(TexturePack& pack, ProcessingLevel level = ProcessingLevel::Full
bool processFolder(TexturePack& pack, ProcessingLevel level = ProcessingLevel::Full);
bool processPackTXT(TexturePack& pack, QByteArray&& raw_data);
-bool processPackPNG(TexturePack& pack, QByteArray&& raw_data);
+bool processPackPNG(const TexturePack& pack, QByteArray&& raw_data);
+
+/// processes ONLY the pack.png (rest of the pack may be invalid)
+bool processPackPNG(const TexturePack& pack);
/** Checks whether a file is valid as a texture pack or not. */
bool validate(QFileInfo file);
diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp
index 3677a1dc..ef353c70 100644
--- a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp
+++ b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp
@@ -103,7 +103,7 @@ void ModFolderLoadTask::executeTask()
while (iter.hasNext()) {
auto mod = iter.next().value();
if (mod->status() == ModStatus::NotInstalled) {
- mod->destroy(m_index_dir, false);
+ mod->destroy(m_index_dir, false, false);
iter.remove();
}
}
diff --git a/launcher/minecraft/services/CapeChange.cpp b/launcher/minecraft/services/CapeChange.cpp
index c73a11b6..1d5ea36d 100644
--- a/launcher/minecraft/services/CapeChange.cpp
+++ b/launcher/minecraft/services/CapeChange.cpp
@@ -54,9 +54,14 @@ void CapeChange::setCape(QString& cape) {
setStatus(tr("Equipping cape"));
m_reply = shared_qobject_ptr<QNetworkReply>(rep);
- connect(rep, &QNetworkReply::uploadProgress, this, &Task::setProgress);
- connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError)));
- connect(rep, SIGNAL(finished()), this, SLOT(downloadFinished()));
+ connect(rep, &QNetworkReply::uploadProgress, this, &CapeChange::setProgress);
+#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15
+ connect(rep, &QNetworkReply::errorOccurred, this, &CapeChange::downloadError);
+#else
+ connect(rep, QOverload<QNetworkReply::NetworkError>::of(&QNetworkReply::error), this, &CapeChange::downloadError);
+#endif
+ connect(rep, &QNetworkReply::sslErrors, this, &CapeChange::sslErrors);
+ connect(rep, &QNetworkReply::finished, this, &CapeChange::downloadFinished);
}
void CapeChange::clearCape() {
@@ -68,13 +73,14 @@ void CapeChange::clearCape() {
setStatus(tr("Removing cape"));
m_reply = shared_qobject_ptr<QNetworkReply>(rep);
- connect(rep, &QNetworkReply::uploadProgress, this, &Task::setProgress);
-#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
- connect(rep, SIGNAL(errorOccurred(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError)));
+ connect(rep, &QNetworkReply::uploadProgress, this, &CapeChange::setProgress);
+#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15
+ connect(rep, &QNetworkReply::errorOccurred, this, &CapeChange::downloadError);
#else
- connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError)));
+ connect(rep, QOverload<QNetworkReply::NetworkError>::of(&QNetworkReply::error), this, &CapeChange::downloadError);
#endif
- connect(rep, SIGNAL(finished()), this, SLOT(downloadFinished()));
+ connect(rep, &QNetworkReply::sslErrors, this, &CapeChange::sslErrors);
+ connect(rep, &QNetworkReply::finished, this, &CapeChange::downloadFinished);
}
@@ -95,6 +101,17 @@ void CapeChange::downloadError(QNetworkReply::NetworkError error)
emitFailed(m_reply->errorString());
}
+void CapeChange::sslErrors(const QList<QSslError>& errors)
+{
+ int i = 1;
+ for (auto error : errors) {
+ qCritical() << "Cape change SSL Error #" << i << " : " << error.errorString();
+ auto cert = error.certificate();
+ qCritical() << "Certificate in question:\n" << cert.toText();
+ i++;
+ }
+}
+
void CapeChange::downloadFinished()
{
// if the download failed
diff --git a/launcher/minecraft/services/CapeChange.h b/launcher/minecraft/services/CapeChange.h
index 185d69b6..38069f90 100644
--- a/launcher/minecraft/services/CapeChange.h
+++ b/launcher/minecraft/services/CapeChange.h
@@ -27,6 +27,7 @@ protected:
public slots:
void downloadError(QNetworkReply::NetworkError);
+ void sslErrors(const QList<QSslError>& errors);
void downloadFinished();
};
diff --git a/launcher/minecraft/services/SkinDelete.cpp b/launcher/minecraft/services/SkinDelete.cpp
index 921bd094..fbaaeacb 100644
--- a/launcher/minecraft/services/SkinDelete.cpp
+++ b/launcher/minecraft/services/SkinDelete.cpp
@@ -53,13 +53,14 @@ void SkinDelete::executeTask()
m_reply = shared_qobject_ptr<QNetworkReply>(rep);
setStatus(tr("Deleting skin"));
- connect(rep, &QNetworkReply::uploadProgress, this, &Task::setProgress);
-#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
- connect(rep, SIGNAL(errorOccurred(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError)));
+ connect(rep, &QNetworkReply::uploadProgress, this, &SkinDelete::setProgress);
+#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15
+ connect(rep, &QNetworkReply::errorOccurred, this, &SkinDelete::downloadError);
#else
- connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError)));
+ connect(rep, QOverload<QNetworkReply::NetworkError>::of(&QNetworkReply::error), this, &SkinDelete::downloadError);
#endif
- connect(rep, SIGNAL(finished()), this, SLOT(downloadFinished()));
+ connect(rep, &QNetworkReply::sslErrors, this, &SkinDelete::sslErrors);
+ connect(rep, &QNetworkReply::finished, this, &SkinDelete::downloadFinished);
}
void SkinDelete::downloadError(QNetworkReply::NetworkError error)
@@ -69,6 +70,17 @@ void SkinDelete::downloadError(QNetworkReply::NetworkError error)
emitFailed(m_reply->errorString());
}
+void SkinDelete::sslErrors(const QList<QSslError>& errors)
+{
+ int i = 1;
+ for (auto error : errors) {
+ qCritical() << "Skin Delete SSL Error #" << i << " : " << error.errorString();
+ auto cert = error.certificate();
+ qCritical() << "Certificate in question:\n" << cert.toText();
+ i++;
+ }
+}
+
void SkinDelete::downloadFinished()
{
// if the download failed
diff --git a/launcher/minecraft/services/SkinDelete.h b/launcher/minecraft/services/SkinDelete.h
index 83a84685..b9a1c9d3 100644
--- a/launcher/minecraft/services/SkinDelete.h
+++ b/launcher/minecraft/services/SkinDelete.h
@@ -22,5 +22,6 @@ protected:
public slots:
void downloadError(QNetworkReply::NetworkError);
+ void sslErrors(const QList<QSslError>& errors);
void downloadFinished();
};
diff --git a/launcher/minecraft/services/SkinUpload.cpp b/launcher/minecraft/services/SkinUpload.cpp
index c7987875..711f8739 100644
--- a/launcher/minecraft/services/SkinUpload.cpp
+++ b/launcher/minecraft/services/SkinUpload.cpp
@@ -78,13 +78,14 @@ void SkinUpload::executeTask()
m_reply = shared_qobject_ptr<QNetworkReply>(rep);
setStatus(tr("Uploading skin"));
- connect(rep, &QNetworkReply::uploadProgress, this, &Task::setProgress);
-#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
- connect(rep, SIGNAL(errorOccurred(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError)));
+ connect(rep, &QNetworkReply::uploadProgress, this, &SkinUpload::setProgress);
+#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15
+ connect(rep, &QNetworkReply::errorOccurred, this, &SkinUpload::downloadError);
#else
- connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError)));
+ connect(rep, QOverload<QNetworkReply::NetworkError>::of(&QNetworkReply::error), this, &SkinUpload::downloadError);
#endif
- connect(rep, SIGNAL(finished()), this, SLOT(downloadFinished()));
+ connect(rep, &QNetworkReply::sslErrors, this, &SkinUpload::sslErrors);
+ connect(rep, &QNetworkReply::finished, this, &SkinUpload::downloadFinished);
}
void SkinUpload::downloadError(QNetworkReply::NetworkError error)
@@ -94,6 +95,17 @@ void SkinUpload::downloadError(QNetworkReply::NetworkError error)
emitFailed(m_reply->errorString());
}
+void SkinUpload::sslErrors(const QList<QSslError>& errors)
+{
+ int i = 1;
+ for (auto error : errors) {
+ qCritical() << "Skin Upload SSL Error #" << i << " : " << error.errorString();
+ auto cert = error.certificate();
+ qCritical() << "Certificate in question:\n" << cert.toText();
+ i++;
+ }
+}
+
void SkinUpload::downloadFinished()
{
// if the download failed
diff --git a/launcher/minecraft/services/SkinUpload.h b/launcher/minecraft/services/SkinUpload.h
index 2c1f0a2e..ac8c5b36 100644
--- a/launcher/minecraft/services/SkinUpload.h
+++ b/launcher/minecraft/services/SkinUpload.h
@@ -32,6 +32,7 @@ protected:
public slots:
void downloadError(QNetworkReply::NetworkError);
+ void sslErrors(const QList<QSslError>& errors);
void downloadFinished();
};
diff --git a/launcher/minecraft/update/AssetUpdateTask.cpp b/launcher/minecraft/update/AssetUpdateTask.cpp
index 8ccb0e1d..31fd5eb1 100644
--- a/launcher/minecraft/update/AssetUpdateTask.cpp
+++ b/launcher/minecraft/update/AssetUpdateTask.cpp
@@ -45,6 +45,7 @@ void AssetUpdateTask::executeTask()
connect(downloadJob.get(), &NetJob::failed, this, &AssetUpdateTask::assetIndexFailed);
connect(downloadJob.get(), &NetJob::aborted, this, [this]{ emitFailed(tr("Aborted")); });
connect(downloadJob.get(), &NetJob::progress, this, &AssetUpdateTask::progress);
+ connect(downloadJob.get(), &NetJob::stepProgress, this, &AssetUpdateTask::propogateStepProgress);
qDebug() << m_inst->name() << ": Starting asset index download";
downloadJob->start();
@@ -83,6 +84,7 @@ void AssetUpdateTask::assetIndexFinished()
connect(downloadJob.get(), &NetJob::failed, this, &AssetUpdateTask::assetsFailed);
connect(downloadJob.get(), &NetJob::aborted, this, [this]{ emitFailed(tr("Aborted")); });
connect(downloadJob.get(), &NetJob::progress, this, &AssetUpdateTask::progress);
+ connect(downloadJob.get(), &NetJob::stepProgress, this, &AssetUpdateTask::propogateStepProgress);
downloadJob->start();
return;
}
diff --git a/launcher/minecraft/update/FMLLibrariesTask.cpp b/launcher/minecraft/update/FMLLibrariesTask.cpp
index 96fd3ba3..75e5c572 100644
--- a/launcher/minecraft/update/FMLLibrariesTask.cpp
+++ b/launcher/minecraft/update/FMLLibrariesTask.cpp
@@ -75,6 +75,7 @@ void FMLLibrariesTask::executeTask()
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);
+ connect(dljob.get(), &NetJob::stepProgress, this, &FMLLibrariesTask::propogateStepProgress);
downloadJob.reset(dljob);
downloadJob->start();
}
diff --git a/launcher/minecraft/update/LibrariesTask.cpp b/launcher/minecraft/update/LibrariesTask.cpp
index b9410111..415b9a66 100644
--- a/launcher/minecraft/update/LibrariesTask.cpp
+++ b/launcher/minecraft/update/LibrariesTask.cpp
@@ -70,6 +70,8 @@ void LibrariesTask::executeTask()
connect(downloadJob.get(), &NetJob::failed, this, &LibrariesTask::jarlibFailed);
connect(downloadJob.get(), &NetJob::aborted, this, [this]{ emitFailed(tr("Aborted")); });
connect(downloadJob.get(), &NetJob::progress, this, &LibrariesTask::progress);
+ connect(downloadJob.get(), &NetJob::stepProgress, this, &LibrariesTask::propogateStepProgress);
+
downloadJob->start();
}
diff --git a/launcher/modplatform/EnsureMetadataTask.cpp b/launcher/modplatform/EnsureMetadataTask.cpp
index 34d969f0..c3eadd06 100644
--- a/launcher/modplatform/EnsureMetadataTask.cpp
+++ b/launcher/modplatform/EnsureMetadataTask.cpp
@@ -10,6 +10,7 @@
#include "modplatform/flame/FlameAPI.h"
#include "modplatform/flame/FlameModIndex.h"
+#include "modplatform/helpers/HashUtils.h"
#include "modplatform/modrinth/ModrinthAPI.h"
#include "modplatform/modrinth/ModrinthPackIndex.h"
@@ -24,8 +25,8 @@ EnsureMetadataTask::EnsureMetadataTask(Mod* mod, QDir dir, ModPlatform::Resource
auto hash_task = createNewHash(mod);
if (!hash_task)
return;
- connect(hash_task.get(), &Task::succeeded, [this, hash_task, mod] { m_mods.insert(hash_task->getResult(), mod); });
- connect(hash_task.get(), &Task::failed, [this, hash_task, mod] { emitFail(mod, "", RemoveFromList::No); });
+ connect(hash_task.get(), &Hashing::Hasher::resultsReady, [this, mod](QString hash) { m_mods.insert(hash, mod); });
+ connect(hash_task.get(), &Task::failed, [this, mod] { emitFail(mod, "", RemoveFromList::No); });
hash_task->start();
}
@@ -37,8 +38,8 @@ EnsureMetadataTask::EnsureMetadataTask(QList<Mod*>& mods, QDir dir, ModPlatform:
auto hash_task = createNewHash(mod);
if (!hash_task)
continue;
- connect(hash_task.get(), &Task::succeeded, [this, hash_task, mod] { m_mods.insert(hash_task->getResult(), mod); });
- connect(hash_task.get(), &Task::failed, [this, hash_task, mod] { emitFail(mod, "", RemoveFromList::No); });
+ connect(hash_task.get(), &Hashing::Hasher::resultsReady, [this, mod](QString hash) { m_mods.insert(hash, mod); });
+ connect(hash_task.get(), &Task::failed, [this, mod] { emitFail(mod, "", RemoveFromList::No); });
m_hashing_task->addTask(hash_task);
}
}
@@ -144,7 +145,8 @@ void EnsureMetadataTask::executeTask()
connect(project_task.get(), &Task::finished, this, [=] {
invalidade_leftover();
project_task->deleteLater();
- m_current_task = nullptr;
+ if (m_current_task)
+ m_current_task.reset();
});
m_current_task = project_task;
@@ -153,7 +155,8 @@ void EnsureMetadataTask::executeTask()
connect(version_task.get(), &Task::finished, [=] {
version_task->deleteLater();
- m_current_task = nullptr;
+ if (m_current_task)
+ m_current_task.reset();
});
if (m_mods.size() > 1)
@@ -212,12 +215,12 @@ Task::Ptr EnsureMetadataTask::modrinthVersionsTask()
{
auto hash_type = ProviderCaps.hashType(ModPlatform::ResourceProvider::MODRINTH).first();
- auto* response = new QByteArray();
+ auto response = std::make_shared<QByteArray>();
auto ver_task = modrinth_api.currentVersions(m_mods.keys(), hash_type, response);
// Prevents unfortunate timings when aborting the task
if (!ver_task)
- return Task::Ptr{nullptr};
+ return Task::Ptr{ nullptr };
connect(ver_task.get(), &Task::succeeded, this, [this, response] {
QJsonParseError parse_error{};
@@ -264,7 +267,7 @@ Task::Ptr EnsureMetadataTask::modrinthProjectsTask()
for (auto const& data : m_temp_versions)
addonIds.insert(data.addonId.toString(), data.hash);
- auto response = new QByteArray();
+ auto response = std::make_shared<QByteArray>();
Task::Ptr proj_task;
if (addonIds.isEmpty()) {
@@ -277,7 +280,7 @@ Task::Ptr EnsureMetadataTask::modrinthProjectsTask()
// Prevents unfortunate timings when aborting the task
if (!proj_task)
- return Task::Ptr{nullptr};
+ return Task::Ptr{ nullptr };
connect(proj_task.get(), &Task::succeeded, this, [this, response, addonIds] {
QJsonParseError parse_error{};
@@ -345,7 +348,7 @@ Task::Ptr EnsureMetadataTask::modrinthProjectsTask()
// Flame
Task::Ptr EnsureMetadataTask::flameVersionsTask()
{
- auto* response = new QByteArray();
+ auto response = std::make_shared<QByteArray>();
QList<uint> fingerprints;
for (auto& murmur : m_mods.keys()) {
@@ -413,7 +416,7 @@ Task::Ptr EnsureMetadataTask::flameProjectsTask()
QHash<QString, QString> addonIds;
for (auto const& hash : m_mods.keys()) {
if (m_temp_versions.contains(hash)) {
- auto const& data = m_temp_versions.find(hash).value();
+ auto data = m_temp_versions.find(hash).value();
auto id_str = data.addonId.toString();
if (!id_str.isEmpty())
@@ -421,7 +424,7 @@ Task::Ptr EnsureMetadataTask::flameProjectsTask()
}
}
- auto response = new QByteArray();
+ auto response = std::make_shared<QByteArray>();
Task::Ptr proj_task;
if (addonIds.isEmpty()) {
@@ -434,7 +437,7 @@ Task::Ptr EnsureMetadataTask::flameProjectsTask()
// Prevents unfortunate timings when aborting the task
if (!proj_task)
- return Task::Ptr{nullptr};
+ return Task::Ptr{ nullptr };
connect(proj_task.get(), &Task::succeeded, this, [this, response, addonIds] {
QJsonParseError parse_error{};
diff --git a/launcher/modplatform/ModIndex.cpp b/launcher/modplatform/ModIndex.cpp
index 6a507caf..a1c4d891 100644
--- a/launcher/modplatform/ModIndex.cpp
+++ b/launcher/modplatform/ModIndex.cpp
@@ -70,11 +70,17 @@ auto ProviderCapabilities::hash(ResourceProvider p, QIODevice* device, QString t
}
QCryptographicHash hash(algo);
- if(!hash.addData(device))
+ if (!hash.addData(device))
qCritical() << "Failed to read JAR to create hash!";
Q_ASSERT(hash.result().length() == hash.hashLength(algo));
return { hash.result().toHex() };
}
+QString getMetaURL(ResourceProvider provider, QVariant projectID)
+{
+ return ((provider == ModPlatform::ResourceProvider::FLAME) ? "https://www.curseforge.com/projects/" : "https://modrinth.com/mod/") +
+ projectID.toString();
+}
+
} // namespace ModPlatform
diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h
index 40f1efc4..2aa91602 100644
--- a/launcher/modplatform/ModIndex.h
+++ b/launcher/modplatform/ModIndex.h
@@ -1,20 +1,21 @@
// 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/>.
-*/
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
+ * Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * 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
@@ -23,6 +24,7 @@
#include <QString>
#include <QVariant>
#include <QVector>
+#include <memory>
class QIODevice;
@@ -32,6 +34,8 @@ enum class ResourceProvider { MODRINTH, FLAME };
enum class ResourceType { MOD, RESOURCE_PACK, SHADER_PACK };
+enum class DependencyType { REQUIRED, OPTIONAL, INCOMPATIBLE, EMBEDDED, TOOL, INCLUDE, UNKNOWN };
+
class ProviderCapabilities {
public:
auto name(ResourceProvider) -> const char*;
@@ -51,6 +55,12 @@ struct DonationData {
QString url;
};
+struct Dependency {
+ QVariant addonId;
+ DependencyType type;
+ QString version;
+};
+
struct IndexedVersion {
QVariant addonId;
QVariant fileId;
@@ -65,10 +75,10 @@ struct IndexedVersion {
QString hash;
bool is_preferred = true;
QString changelog;
+ QList<Dependency> dependencies;
// For internal use, not provided by APIs
bool is_currently_selected = false;
- QString custom_target_folder;
};
struct ExtraPackData {
@@ -83,6 +93,8 @@ struct ExtraPackData {
};
struct IndexedPack {
+ using Ptr = std::shared_ptr<IndexedPack>;
+
QVariant addonId;
ResourceProvider provider;
QString name;
@@ -113,12 +125,30 @@ struct IndexedPack {
if (!versionsLoaded)
return false;
- return std::any_of(versions.constBegin(), versions.constEnd(),
- [](auto const& v) { return v.is_currently_selected; });
+ return std::any_of(versions.constBegin(), versions.constEnd(), [](auto const& v) { return v.is_currently_selected; });
}
};
+QString getMetaURL(ResourceProvider provider, QVariant projectID);
+
+struct OverrideDep {
+ QString quilt;
+ QString fabric;
+ QString slug;
+ ModPlatform::ResourceProvider provider;
+};
+
+inline auto getOverrideDeps() -> QList<OverrideDep>
+{
+ return { { "634179", "306612", "API", ModPlatform::ResourceProvider::FLAME },
+ { "720410", "308769", "KotlinLibraries", ModPlatform::ResourceProvider::FLAME },
+
+ { "qvIfYCYJ", "P7dR8mSH", "API", ModPlatform::ResourceProvider::MODRINTH },
+ { "lwVhp9o5", "Ha28R6CL", "KotlinLibraries", ModPlatform::ResourceProvider::MODRINTH } };
+};
+QString getMetaURL(ResourceProvider provider, QVariant projectID);
} // namespace ModPlatform
Q_DECLARE_METATYPE(ModPlatform::IndexedPack)
+Q_DECLARE_METATYPE(ModPlatform::IndexedPack::Ptr)
Q_DECLARE_METATYPE(ModPlatform::ResourceProvider)
diff --git a/launcher/modplatform/ResourceAPI.h b/launcher/modplatform/ResourceAPI.h
index 34f33779..d3277761 100644
--- a/launcher/modplatform/ResourceAPI.h
+++ b/launcher/modplatform/ResourceAPI.h
@@ -111,6 +111,16 @@ class ResourceAPI {
std::function<void(QJsonDocument&, ModPlatform::IndexedPack)> on_succeed;
};
+ struct DependencySearchArgs {
+ ModPlatform::Dependency dependency;
+ Version mcVersion;
+ ModLoaderTypes loader;
+ };
+
+ struct DependencySearchCallbacks {
+ std::function<void(QJsonDocument&, const ModPlatform::Dependency&)> on_succeed;
+ };
+
public:
/** Gets a list of available sorting methods for this API. */
[[nodiscard]] virtual auto getSortingMethods() const -> QList<SortingMethod> = 0;
@@ -121,12 +131,12 @@ class ResourceAPI {
qWarning() << "TODO";
return nullptr;
}
- [[nodiscard]] virtual Task::Ptr getProject(QString addonId, QByteArray* response) const
+ [[nodiscard]] virtual Task::Ptr getProject(QString addonId, std::shared_ptr<QByteArray> response) const
{
qWarning() << "TODO";
return nullptr;
}
- [[nodiscard]] virtual Task::Ptr getProjects(QStringList addonIds, QByteArray* response) const
+ [[nodiscard]] virtual Task::Ptr getProjects(QStringList addonIds, std::shared_ptr<QByteArray> response) const
{
qWarning() << "TODO";
return nullptr;
@@ -143,6 +153,12 @@ class ResourceAPI {
return nullptr;
}
+ [[nodiscard]] virtual Task::Ptr getDependencyVersion(DependencySearchArgs&&, DependencySearchCallbacks&&) const
+ {
+ qWarning() << "TODO";
+ return nullptr;
+ }
+
static auto getModLoaderString(ModLoaderType type) -> const QString
{
switch (type) {
diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp
index 4bd8b7f2..22ea02da 100644
--- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp
+++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp
@@ -82,9 +82,9 @@ void PackInstallTask::executeTask()
{
qDebug() << "PackInstallTask::executeTask: " << QThread::currentThreadId();
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));
+ 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);
@@ -99,11 +99,12 @@ void PackInstallTask::onDownloadSucceeded()
qDebug() << "PackInstallTask::onDownloadSucceeded: " << QThread::currentThreadId();
jobPtr.reset();
- QJsonParseError parse_error {};
- QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error);
- if(parse_error.error != QJsonParseError::NoError) {
- qWarning() << "Error while parsing JSON response from ATLauncher at " << parse_error.offset << " reason: " << parse_error.errorString();
- qWarning() << response;
+ QJsonParseError parse_error{};
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
+ if (parse_error.error != QJsonParseError::NoError) {
+ qWarning() << "Error while parsing JSON response from ATLauncher at " << parse_error.offset
+ << " reason: " << parse_error.errorString();
+ qWarning() << *response.get();
return;
}
auto obj = doc.object();
@@ -352,7 +353,7 @@ QString PackInstallTask::getVersionForLoader(QString uid)
if(m_version.loader.recommended || m_version.loader.latest) {
for (int i = 0; i < vlist->versions().size(); i++) {
auto version = vlist->versions().at(i);
- auto reqs = version->requires();
+ auto reqs = version->requiredSet();
// filter by minecraft version, if the loader depends on a certain version.
// not all mod loaders depend on a given Minecraft version, so we won't do this
@@ -683,6 +684,7 @@ void PackInstallTask::installConfigs()
abortable = true;
setProgress(current, total);
});
+ connect(jobPtr.get(), &NetJob::stepProgress, this, &PackInstallTask::propogateStepProgress);
connect(jobPtr.get(), &NetJob::aborted, [&]{
abortable = false;
jobPtr.reset();
@@ -846,9 +848,11 @@ void PackInstallTask::downloadMods()
});
connect(jobPtr.get(), &NetJob::progress, [&](qint64 current, qint64 total)
{
+ setDetails(tr("%1 out of %2 complete").arg(current).arg(total));
abortable = true;
setProgress(current, total);
});
+ connect(jobPtr.get(), &NetJob::stepProgress, this, &PackInstallTask::propogateStepProgress);
connect(jobPtr.get(), &NetJob::aborted, [&]
{
abortable = false;
diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.h b/launcher/modplatform/atlauncher/ATLPackInstallTask.h
index 90e25ae2..b82f523f 100644
--- a/launcher/modplatform/atlauncher/ATLPackInstallTask.h
+++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.h
@@ -40,12 +40,13 @@
#include "ATLPackManifest.h"
#include "InstanceTask.h"
-#include "net/NetJob.h"
-#include "settings/INISettingsObject.h"
+#include "meta/Version.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
-#include "meta/Version.h"
+#include "net/NetJob.h"
+#include "settings/INISettingsObject.h"
+#include <memory>
#include <optional>
namespace ATLauncher {
@@ -57,8 +58,7 @@ enum class InstallMode {
};
class UserInteractionSupport {
-
-public:
+ public:
/**
* Requests a user interaction to select which optional mods should be installed.
*/
@@ -74,23 +74,27 @@ public:
* Requests a user interaction to display a message.
*/
virtual void displayMessage(QString message) = 0;
+
+ virtual ~UserInteractionSupport() = default;
};
-class PackInstallTask : public InstanceTask
-{
-Q_OBJECT
+class PackInstallTask : public InstanceTask {
+ Q_OBJECT
-public:
- explicit PackInstallTask(UserInteractionSupport *support, QString packName, QString version, InstallMode installMode = InstallMode::Install);
- virtual ~PackInstallTask(){}
+ public:
+ explicit PackInstallTask(UserInteractionSupport* support,
+ QString packName,
+ QString version,
+ InstallMode installMode = InstallMode::Install);
+ virtual ~PackInstallTask() { delete m_support; }
bool canAbort() const override { return true; }
bool abort() override;
-protected:
+ protected:
virtual void executeTask() override;
-private slots:
+ private slots:
void onDownloadSucceeded();
void onDownloadFailed(QString reason);
void onDownloadAborted();
@@ -98,7 +102,7 @@ private slots:
void onModsDownloaded();
void onModsExtracted();
-private:
+ private:
QString getDirForModType(ModType type, QString raw);
QString getVersionForLoader(QString uid);
QString detectLibrary(VersionLibrary library);
@@ -110,20 +114,18 @@ private:
void installConfigs();
void extractConfigs();
void downloadMods();
- bool extractMods(
- const QMap<QString, VersionMod> &toExtract,
- const QMap<QString, VersionMod> &toDecomp,
- const QMap<QString, QString> &toCopy
- );
+ bool extractMods(const QMap<QString, VersionMod>& toExtract,
+ const QMap<QString, VersionMod>& toDecomp,
+ const QMap<QString, QString>& toCopy);
void install();
-private:
- UserInteractionSupport *m_support;
+ private:
+ UserInteractionSupport* m_support;
bool abortable = false;
NetJob::Ptr jobPtr;
- QByteArray response;
+ std::shared_ptr<QByteArray> response = std::make_shared<QByteArray>();
InstallMode m_install_mode;
QString m_pack_name;
@@ -145,7 +147,6 @@ private:
QFuture<bool> m_modExtractFuture;
QFutureWatcher<bool> m_modExtractFutureWatcher;
-
};
-}
+} // namespace ATLauncher
diff --git a/launcher/modplatform/flame/FileResolvingTask.cpp b/launcher/modplatform/flame/FileResolvingTask.cpp
index d3a737bb..34bd401d 100644
--- a/launcher/modplatform/flame/FileResolvingTask.cpp
+++ b/launcher/modplatform/flame/FileResolvingTask.cpp
@@ -21,21 +21,48 @@ bool Flame::FileResolvingTask::abort()
void Flame::FileResolvingTask::executeTask()
{
+ if (m_toProcess.files.isEmpty()) { // no file to resolve so leave it empty and emit success immediately
+ emitSucceeded();
+ return;
+ }
setStatus(tr("Resolving mod IDs..."));
setProgress(0, 3);
m_dljob.reset(new NetJob("Mod id resolver", m_network));
result.reset(new QByteArray());
- //build json data to send
+ // build json data to send
QJsonObject object;
- object["fileIds"] = QJsonArray::fromVariantList(std::accumulate(m_toProcess.files.begin(), m_toProcess.files.end(), QVariantList(), [](QVariantList& l, const File& s) {
- l.push_back(s.fileId);
- return l;
- }));
+ object["fileIds"] = QJsonArray::fromVariantList(
+ std::accumulate(m_toProcess.files.begin(), m_toProcess.files.end(), QVariantList(), [](QVariantList& l, const File& s) {
+ l.push_back(s.fileId);
+ return l;
+ }));
QByteArray data = Json::toText(object);
- auto dl = Net::Upload::makeByteArray(QUrl("https://api.curseforge.com/v1/mods/files"), result.get(), data);
+ auto dl = Net::Upload::makeByteArray(QUrl("https://api.curseforge.com/v1/mods/files"), result, data);
m_dljob->addNetAction(dl);
- connect(m_dljob.get(), &NetJob::finished, this, &Flame::FileResolvingTask::netJobFinished);
+
+ auto step_progress = std::make_shared<TaskStepProgress>();
+ connect(m_dljob.get(), &NetJob::finished, this, [this, step_progress]() {
+ step_progress->state = TaskStepState::Succeeded;
+ stepProgress(*step_progress);
+ netJobFinished();
+ });
+ connect(m_dljob.get(), &NetJob::failed, this, [this, step_progress](QString reason) {
+ step_progress->state = TaskStepState::Failed;
+ stepProgress(*step_progress);
+ emitFailed(reason);
+ });
+ connect(m_dljob.get(), &NetJob::stepProgress, this, &FileResolvingTask::propogateStepProgress);
+ connect(m_dljob.get(), &NetJob::progress, this, [this, step_progress](qint64 current, qint64 total) {
+ qDebug() << "Resolve slug progress" << current << total;
+ step_progress->update(current, total);
+ stepProgress(*step_progress);
+ });
+ connect(m_dljob.get(), &NetJob::status, this, [this, step_progress](QString status) {
+ step_progress->status = status;
+ stepProgress(*step_progress);
+ });
+
m_dljob->start();
}
@@ -44,7 +71,7 @@ void Flame::FileResolvingTask::netJobFinished()
setProgress(1, 3);
// job to check modrinth for blocked projects
m_checkJob.reset(new NetJob("Modrinth check", m_network));
- blockedProjects = QMap<File *,QByteArray *>();
+ blockedProjects = QMap<File*, std::shared_ptr<QByteArray>>();
QJsonDocument doc;
QJsonArray array;
@@ -65,37 +92,55 @@ void Flame::FileResolvingTask::netJobFinished()
auto fileid = Json::requireInteger(Json::requireObject(file)["id"]);
auto& out = m_toProcess.files[fileid];
try {
- out.parseFromObject(Json::requireObject(file));
+ out.parseFromObject(Json::requireObject(file));
} catch (const JSONValidationError& e) {
qDebug() << "Blocked mod on curseforge" << out.fileName;
auto hash = out.hash;
- if(!hash.isEmpty()) {
+ if (!hash.isEmpty()) {
auto url = QString("https://api.modrinth.com/v2/version_file/%1?algorithm=sha1").arg(hash);
- auto output = new QByteArray();
+ auto output = std::make_shared<QByteArray>();
auto dl = Net::Download::makeByteArray(QUrl(url), output);
- QObject::connect(dl.get(), &Net::Download::succeeded, [&out]() {
- out.resolved = true;
- });
+ QObject::connect(dl.get(), &Net::Download::succeeded, [&out]() { out.resolved = true; });
m_checkJob->addNetAction(dl);
blockedProjects.insert(&out, output);
}
}
}
- connect(m_checkJob.get(), &NetJob::finished, this, &Flame::FileResolvingTask::modrinthCheckFinished);
+ auto step_progress = std::make_shared<TaskStepProgress>();
+ connect(m_checkJob.get(), &NetJob::finished, this, [this, step_progress]() {
+ step_progress->state = TaskStepState::Succeeded;
+ stepProgress(*step_progress);
+ modrinthCheckFinished();
+ });
+ connect(m_checkJob.get(), &NetJob::failed, this, [this, step_progress](QString reason) {
+ step_progress->state = TaskStepState::Failed;
+ stepProgress(*step_progress);
+ emitFailed(reason);
+ });
+ connect(m_checkJob.get(), &NetJob::stepProgress, this, &FileResolvingTask::propogateStepProgress);
+ connect(m_checkJob.get(), &NetJob::progress, this, [this, step_progress](qint64 current, qint64 total) {
+ qDebug() << "Resolve slug progress" << current << total;
+ step_progress->update(current, total);
+ stepProgress(*step_progress);
+ });
+ connect(m_checkJob.get(), &NetJob::status, this, [this, step_progress](QString status) {
+ step_progress->status = status;
+ stepProgress(*step_progress);
+ });
m_checkJob->start();
}
-void Flame::FileResolvingTask::modrinthCheckFinished() {
+void Flame::FileResolvingTask::modrinthCheckFinished()
+{
setProgress(2, 3);
qDebug() << "Finished with blocked mods : " << blockedProjects.size();
for (auto it = blockedProjects.keyBegin(); it != blockedProjects.keyEnd(); it++) {
- auto &out = *it;
+ auto& out = *it;
auto bytes = blockedProjects[out];
if (!out->resolved) {
- delete bytes;
continue;
}
@@ -112,44 +157,56 @@ void Flame::FileResolvingTask::modrinthCheckFinished() {
} else {
out->resolved = false;
}
-
- delete bytes;
}
- //copy to an output list and filter out projects found on modrinth
- auto block = new QList<File *>();
+ // copy to an output list and filter out projects found on modrinth
+ auto block = std::make_shared<QList<File*>>();
auto it = blockedProjects.keys();
- std::copy_if(it.begin(), it.end(), std::back_inserter(*block), [](File *f) {
- return !f->resolved;
- });
- //Display not found mods early
+ std::copy_if(it.begin(), it.end(), std::back_inserter(*block), [](File* f) { return !f->resolved; });
+ // Display not found mods early
if (!block->empty()) {
- //blocked mods found, we need the slug for displaying.... we need another job :D !
- auto slugJob = new NetJob("Slug Job", m_network);
- auto slugs = QVector<QByteArray>(block->size());
- auto index = 0;
- for (auto fileInfo: *block) {
- auto projectId = fileInfo->projectId;
- slugs[index] = QByteArray();
+ // blocked mods found, we need the slug for displaying.... we need another job :D !
+ m_slugJob.reset(new NetJob("Slug Job", m_network));
+ int index = 0;
+ for (auto mod : *block) {
+ auto projectId = mod->projectId;
+ auto output = std::make_shared<QByteArray>();
auto url = QString("https://api.curseforge.com/v1/mods/%1").arg(projectId);
- auto dl = Net::Download::makeByteArray(url, &slugs[index]);
- slugJob->addNetAction(dl);
- index++;
- }
- connect(slugJob, &NetJob::succeeded, this, [slugs, this, slugJob, block]() {
- slugJob->deleteLater();
- auto index = 0;
- for (const auto &slugResult: slugs) {
- auto json = QJsonDocument::fromJson(slugResult);
- auto base = Json::requireString(Json::requireObject(Json::requireObject(Json::requireObject(json),"data"),"links"),
- "websiteUrl");
- auto mod = block->at(index);
+ auto dl = Net::Download::makeByteArray(url, output);
+ qDebug() << "Fetching url slug for file:" << mod->fileName;
+ QObject::connect(dl.get(), &Net::Download::succeeded, [block, index, output]() {
+ auto mod = block->at(index); // use the shared_ptr so it is captured and only freed when we are done
+ auto json = QJsonDocument::fromJson(*output);
+ auto base =
+ Json::requireString(Json::requireObject(Json::requireObject(Json::requireObject(json), "data"), "links"), "websiteUrl");
auto link = QString("%1/download/%2").arg(base, QString::number(mod->fileId));
mod->websiteUrl = link;
- index++;
- }
+ });
+ m_slugJob->addNetAction(dl);
+ index++;
+ }
+ auto step_progress = std::make_shared<TaskStepProgress>();
+ connect(m_slugJob.get(), &NetJob::succeeded, this, [this, step_progress]() {
+ step_progress->state = TaskStepState::Succeeded;
+ stepProgress(*step_progress);
emitSucceeded();
});
- slugJob->start();
+ connect(m_slugJob.get(), &NetJob::failed, this, [this, step_progress](QString reason) {
+ step_progress->state = TaskStepState::Failed;
+ stepProgress(*step_progress);
+ emitFailed(reason);
+ });
+ connect(m_slugJob.get(), &NetJob::stepProgress, this, &FileResolvingTask::propogateStepProgress);
+ connect(m_slugJob.get(), &NetJob::progress, this, [this, step_progress](qint64 current, qint64 total) {
+ qDebug() << "Resolve slug progress" << current << total;
+ step_progress->update(current, total);
+ stepProgress(*step_progress);
+ });
+ connect(m_slugJob.get(), &NetJob::status, this, [this, step_progress](QString status) {
+ step_progress->status = status;
+ stepProgress(*step_progress);
+ });
+
+ m_slugJob->start();
} else {
emitSucceeded();
}
diff --git a/launcher/modplatform/flame/FileResolvingTask.h b/launcher/modplatform/flame/FileResolvingTask.h
index 8fc17ea9..c280827a 100644
--- a/launcher/modplatform/flame/FileResolvingTask.h
+++ b/launcher/modplatform/flame/FileResolvingTask.h
@@ -1,41 +1,37 @@
#pragma once
-#include "tasks/Task.h"
-#include "net/NetJob.h"
#include "PackManifest.h"
+#include "net/NetJob.h"
+#include "tasks/Task.h"
-namespace Flame
-{
-class FileResolvingTask : public Task
-{
+namespace Flame {
+class FileResolvingTask : public Task {
Q_OBJECT
-public:
- explicit FileResolvingTask(const shared_qobject_ptr<QNetworkAccessManager>& network, Flame::Manifest &toProcess);
- virtual ~FileResolvingTask() {};
+ public:
+ explicit FileResolvingTask(const shared_qobject_ptr<QNetworkAccessManager>& network, Flame::Manifest& toProcess);
+ virtual ~FileResolvingTask(){};
bool canAbort() const override { return true; }
bool abort() override;
- const Flame::Manifest &getResults() const
- {
- return m_toProcess;
- }
+ const Flame::Manifest& getResults() const { return m_toProcess; }
-protected:
+ protected:
virtual void executeTask() override;
-protected slots:
+ protected slots:
void netJobFinished();
-private: /* data */
+ private: /* data */
shared_qobject_ptr<QNetworkAccessManager> m_network;
Flame::Manifest m_toProcess;
- std::shared_ptr<QByteArray> result;
+ std::shared_ptr<QByteArray> result;
NetJob::Ptr m_dljob;
- NetJob::Ptr m_checkJob;
+ NetJob::Ptr m_checkJob;
+ NetJob::Ptr m_slugJob;
void modrinthCheckFinished();
- QMap<File *, QByteArray *> blockedProjects;
+ QMap<File*, std::shared_ptr<QByteArray>> blockedProjects;
};
-}
+} // namespace Flame
diff --git a/launcher/modplatform/flame/FlameAPI.cpp b/launcher/modplatform/flame/FlameAPI.cpp
index 5ef9a409..5b0b1d8b 100644
--- a/launcher/modplatform/flame/FlameAPI.cpp
+++ b/launcher/modplatform/flame/FlameAPI.cpp
@@ -11,7 +11,7 @@
#include "net/NetJob.h"
#include "net/Upload.h"
-Task::Ptr FlameAPI::matchFingerprints(const QList<uint>& fingerprints, QByteArray* response)
+Task::Ptr FlameAPI::matchFingerprints(const QList<uint>& fingerprints, std::shared_ptr<QByteArray> response)
{
auto netJob = makeShared<NetJob>(QString("Flame::MatchFingerprints"), APPLICATION->network());
@@ -28,8 +28,6 @@ Task::Ptr FlameAPI::matchFingerprints(const QList<uint>& fingerprints, QByteArra
netJob->addNetAction(Net::Upload::makeByteArray(QString("https://api.curseforge.com/v1/fingerprints"), response, body_raw));
- QObject::connect(netJob.get(), &NetJob::finished, [response] { delete response; });
-
return netJob;
}
@@ -38,14 +36,14 @@ auto FlameAPI::getModFileChangelog(int modId, int fileId) -> QString
QEventLoop lock;
QString changelog;
- auto* netJob = new NetJob(QString("Flame::FileChangelog"), APPLICATION->network());
- auto* response = new QByteArray();
+ auto netJob = makeShared<NetJob>(QString("Flame::FileChangelog"), APPLICATION->network());
+ auto response = std::make_shared<QByteArray>();
netJob->addNetAction(Net::Download::makeByteArray(
QString("https://api.curseforge.com/v1/mods/%1/files/%2/changelog")
.arg(QString::fromStdString(std::to_string(modId)), QString::fromStdString(std::to_string(fileId))),
response));
- QObject::connect(netJob, &NetJob::succeeded, [netJob, response, &changelog] {
+ QObject::connect(netJob.get(), &NetJob::succeeded, [&netJob, response, &changelog] {
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
@@ -60,10 +58,7 @@ auto FlameAPI::getModFileChangelog(int modId, int fileId) -> QString
changelog = Json::ensureString(doc.object(), "data");
});
- QObject::connect(netJob, &NetJob::finished, [response, &lock] {
- delete response;
- lock.quit();
- });
+ QObject::connect(netJob.get(), &NetJob::finished, [&lock] { lock.quit(); });
netJob->start();
lock.exec();
@@ -76,13 +71,12 @@ auto FlameAPI::getModDescription(int modId) -> QString
QEventLoop lock;
QString description;
- auto* netJob = new NetJob(QString("Flame::ModDescription"), APPLICATION->network());
- auto* response = new QByteArray();
- netJob->addNetAction(Net::Download::makeByteArray(
- QString("https://api.curseforge.com/v1/mods/%1/description")
- .arg(QString::number(modId)), response));
+ auto netJob = makeShared<NetJob>(QString("Flame::ModDescription"), APPLICATION->network());
+ auto response = std::make_shared<QByteArray>();
+ netJob->addNetAction(
+ Net::Download::makeByteArray(QString("https://api.curseforge.com/v1/mods/%1/description").arg(QString::number(modId)), response));
- QObject::connect(netJob, &NetJob::succeeded, [netJob, response, &description] {
+ QObject::connect(netJob.get(), &NetJob::succeeded, [&netJob, response, &description] {
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
@@ -97,10 +91,7 @@ auto FlameAPI::getModDescription(int modId) -> QString
description = Json::ensureString(doc.object(), "data");
});
- QObject::connect(netJob, &NetJob::finished, [response, &lock] {
- delete response;
- lock.quit();
- });
+ QObject::connect(netJob.get(), &NetJob::finished, [&lock] { lock.quit(); });
netJob->start();
lock.exec();
@@ -118,13 +109,13 @@ auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::Indexe
QEventLoop loop;
- auto netJob = new NetJob(QString("Flame::GetLatestVersion(%1)").arg(args.pack.name), APPLICATION->network());
- auto response = new QByteArray();
+ auto netJob = makeShared<NetJob>(QString("Flame::GetLatestVersion(%1)").arg(args.pack.name), APPLICATION->network());
+ auto response = std::make_shared<QByteArray>();
ModPlatform::IndexedVersion ver;
netJob->addNetAction(Net::Download::makeByteArray(versions_url, response));
- QObject::connect(netJob, &NetJob::succeeded, [response, args, &ver] {
+ QObject::connect(netJob.get(), &NetJob::succeeded, [response, args, &ver] {
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
@@ -144,7 +135,7 @@ auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::Indexe
for (auto file : arr) {
auto file_obj = Json::requireObject(file);
auto file_tmp = FlameMod::loadIndexedPackVersion(file_obj);
- if(file_tmp.date > ver_tmp.date) {
+ if (file_tmp.date > ver_tmp.date) {
ver_tmp = file_tmp;
latest_file_obj = file_obj;
}
@@ -158,11 +149,7 @@ auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::Indexe
}
});
- QObject::connect(netJob, &NetJob::finished, [response, netJob, &loop] {
- netJob->deleteLater();
- delete response;
- loop.quit();
- });
+ QObject::connect(netJob.get(), &NetJob::finished, [&loop] { loop.quit(); });
netJob->start();
@@ -171,7 +158,7 @@ auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::Indexe
return ver;
}
-Task::Ptr FlameAPI::getProjects(QStringList addonIds, QByteArray* response) const
+Task::Ptr FlameAPI::getProjects(QStringList addonIds, std::shared_ptr<QByteArray> response) const
{
auto netJob = makeShared<NetJob>(QString("Flame::GetProjects"), APPLICATION->network());
@@ -188,13 +175,12 @@ Task::Ptr FlameAPI::getProjects(QStringList addonIds, QByteArray* response) cons
netJob->addNetAction(Net::Upload::makeByteArray(QString("https://api.curseforge.com/v1/mods"), response, body_raw));
- QObject::connect(netJob.get(), &NetJob::finished, [response] { delete response; });
QObject::connect(netJob.get(), &NetJob::failed, [body_raw] { qDebug() << body_raw; });
return netJob;
}
-Task::Ptr FlameAPI::getFiles(const QStringList& fileIds, QByteArray* response) const
+Task::Ptr FlameAPI::getFiles(const QStringList& fileIds, std::shared_ptr<QByteArray> response) const
{
auto netJob = makeShared<NetJob>(QString("Flame::GetFiles"), APPLICATION->network());
@@ -211,7 +197,6 @@ Task::Ptr FlameAPI::getFiles(const QStringList& fileIds, QByteArray* response) c
netJob->addNetAction(Net::Upload::makeByteArray(QString("https://api.curseforge.com/v1/mods/files"), response, body_raw));
- QObject::connect(netJob.get(), &NetJob::finished, [response] { delete response; });
QObject::connect(netJob.get(), &NetJob::failed, [body_raw] { qDebug() << body_raw; });
return netJob;
diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h
index 5811d717..49bc316f 100644
--- a/launcher/modplatform/flame/FlameAPI.h
+++ b/launcher/modplatform/flame/FlameAPI.h
@@ -4,7 +4,10 @@
#pragma once
+#include <algorithm>
+#include <memory>
#include "modplatform/ModIndex.h"
+#include "modplatform/ResourceAPI.h"
#include "modplatform/helpers/NetworkResourceAPI.h"
class FlameAPI : public NetworkResourceAPI {
@@ -14,12 +17,14 @@ class FlameAPI : public NetworkResourceAPI {
auto getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::IndexedVersion;
- 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;
+ Task::Ptr getProjects(QStringList addonIds, std::shared_ptr<QByteArray> response) const override;
+ Task::Ptr matchFingerprints(const QList<uint>& fingerprints, std::shared_ptr<QByteArray> response);
+ Task::Ptr getFiles(const QStringList& fileIds, std::shared_ptr<QByteArray> response) const;
[[nodiscard]] auto getSortingMethods() const -> QList<ResourceAPI::SortingMethod> override;
+ static inline auto validateModLoaders(ModLoaderTypes loaders) -> bool { return loaders & (Forge | Fabric | Quilt); }
+
private:
static int getClassId(ModPlatform::ResourceType type)
{
@@ -41,14 +46,15 @@ class FlameAPI : public NetworkResourceAPI {
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 4; // Quilt would probably be 5
return 0;
}
private:
[[nodiscard]] std::optional<QString> getSearchURL(SearchArgs const& args) const override
{
- auto gameVersionStr = args.versions.has_value() ? QString("gameVersion=%1").arg(args.versions.value().front().toString()) : QString();
+ auto gameVersionStr =
+ args.versions.has_value() ? QString("gameVersion=%1").arg(args.versions.value().front().toString()) : QString();
QStringList get_arguments;
get_arguments.append(QString("classId=%1").arg(getClassId(args.type)));
@@ -73,14 +79,48 @@ class FlameAPI : public NetworkResourceAPI {
[[nodiscard]] std::optional<QString> getVersionsURL(VersionSearchArgs const& args) const override
{
- QString url{QString("https://api.curseforge.com/v1/mods/%1/files?pageSize=10000&").arg(args.pack.addonId.toString())};
+ auto addonId = args.pack.addonId.toString();
+ QString url{ QString("https://api.curseforge.com/v1/mods/%1/files?pageSize=10000&").arg(addonId) };
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())));
+
+ if (args.loaders.has_value()) {
+ int mappedModLoader = getMappedModLoader(args.loaders.value());
+
+ if (args.loaders.value() & Quilt) {
+ auto overide = ModPlatform::getOverrideDeps();
+ auto over = std::find_if(overide.cbegin(), overide.cend(), [addonId](auto dep) {
+ return dep.provider == ModPlatform::ResourceProvider::FLAME && addonId == dep.quilt;
+ });
+ if (over != overide.cend()) {
+ mappedModLoader = 5;
+ }
+ }
+
+ get_parameters.append(QString("modLoaderType=%1").arg(mappedModLoader));
+ }
return url + get_parameters.join('&');
};
+
+ [[nodiscard]] std::optional<QString> getDependencyURL(DependencySearchArgs const& args) const override
+ {
+ auto mappedModLoader = getMappedModLoader(args.loader);
+ auto addonId = args.dependency.addonId.toString();
+ if (args.loader & Quilt) {
+ auto overide = ModPlatform::getOverrideDeps();
+ auto over = std::find_if(overide.cbegin(), overide.cend(), [addonId](auto dep) {
+ return dep.provider == ModPlatform::ResourceProvider::FLAME && addonId == dep.quilt;
+ });
+ if (over != overide.cend()) {
+ mappedModLoader = 5;
+ }
+ }
+ return QString("https://api.curseforge.com/v1/mods/%1/files?pageSize=10000&gameVersion=%2&modLoaderType=%3")
+ .arg(addonId)
+ .arg(args.mcVersion.toString())
+ .arg(mappedModLoader);
+ };
};
diff --git a/launcher/modplatform/flame/FlameCheckUpdate.cpp b/launcher/modplatform/flame/FlameCheckUpdate.cpp
index 06a89502..a2628e34 100644
--- a/launcher/modplatform/flame/FlameCheckUpdate.cpp
+++ b/launcher/modplatform/flame/FlameCheckUpdate.cpp
@@ -3,6 +3,7 @@
#include "FlameModIndex.h"
#include <MurmurHash2.h>
+#include <memory>
#include "FileSystem.h"
#include "Json.h"
@@ -30,7 +31,7 @@ ModPlatform::IndexedPack getProjectInfo(ModPlatform::IndexedVersion& ver_info)
auto get_project_job = new NetJob("Flame::GetProjectJob", APPLICATION->network());
- auto response = new QByteArray();
+ auto response = std::make_shared<QByteArray>();
auto url = QString("https://api.curseforge.com/v1/mods/%1").arg(ver_info.addonId.toString());
auto dl = Net::Download::makeByteArray(url, response);
get_project_job->addNetAction(dl);
@@ -74,7 +75,7 @@ ModPlatform::IndexedVersion getFileInfo(int addonId, int fileId)
auto get_file_info_job = new NetJob("Flame::GetFileInfoJob", APPLICATION->network());
- auto response = new QByteArray();
+ auto response = std::make_shared<QByteArray>();
auto url = QString("https://api.curseforge.com/v1/mods/%1/files/%2").arg(QString::number(addonId), QString::number(fileId));
auto dl = Net::Download::makeByteArray(url, response);
get_file_info_job->addNetAction(dl);
@@ -129,8 +130,7 @@ void FlameCheckUpdate::executeTask()
setStatus(tr("Getting API response from CurseForge for '%1'...").arg(mod->name()));
setProgress(i++, m_mods.size());
- ModPlatform::IndexedPack pack{ mod->metadata()->project_id.toString() };
- auto latest_ver = api.getLatestVersion({ pack, m_game_versions, m_loaders });
+ auto latest_ver = api.getLatestVersion({ { mod->metadata()->project_id.toString() }, m_game_versions, m_loaders });
// Check if we were aborted while getting the latest version
if (m_was_aborted) {
@@ -156,15 +156,15 @@ void FlameCheckUpdate::executeTask()
if (!latest_ver.hash.isEmpty() && (mod->metadata()->hash != latest_ver.hash || mod->status() == ModStatus::NotInstalled)) {
// Fake pack with the necessary info to pass to the download task :)
- ModPlatform::IndexedPack pack;
- pack.name = mod->name();
- pack.slug = mod->metadata()->slug;
- pack.addonId = mod->metadata()->project_id;
- pack.websiteUrl = mod->homeurl();
+ auto pack = std::make_shared<ModPlatform::IndexedPack>();
+ pack->name = mod->name();
+ pack->slug = mod->metadata()->slug;
+ pack->addonId = mod->metadata()->project_id;
+ pack->websiteUrl = mod->homeurl();
for (auto& author : mod->authors())
- pack.authors.append({ author });
- pack.description = mod->description();
- pack.provider = ModPlatform::ResourceProvider::FLAME;
+ pack->authors.append({ author });
+ pack->description = mod->description();
+ pack->provider = ModPlatform::ResourceProvider::FLAME;
auto old_version = mod->version();
if (old_version.isEmpty() && mod->status() != ModStatus::NotInstalled) {
@@ -173,7 +173,7 @@ void FlameCheckUpdate::executeTask()
}
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,
+ 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::ResourceProvider::FLAME, download_task);
}
diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp
index 964b559c..b57db288 100644
--- a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp
+++ b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp
@@ -35,6 +35,7 @@
#include "FlameInstanceCreationTask.h"
+#include "modplatform/flame/FileResolvingTask.h"
#include "modplatform/flame/FlameAPI.h"
#include "modplatform/flame/PackManifest.h"
@@ -152,6 +153,9 @@ bool FlameCreationTask::updateInstance()
old_files.remove(file.key());
files_iterator = files.erase(files_iterator);
+
+ if (files_iterator != files.begin())
+ files_iterator--;
}
}
@@ -178,7 +182,7 @@ bool FlameCreationTask::updateInstance()
fileIds.append(QString::number(file.fileId));
}
- auto* raw_response = new QByteArray;
+ auto raw_response = std::make_shared<QByteArray>();
auto job = api.getFiles(fileIds, raw_response);
QEventLoop loop;
@@ -382,7 +386,8 @@ bool FlameCreationTask::createInstance()
});
connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::progress, this, &FlameCreationTask::setProgress);
connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::status, this, &FlameCreationTask::setStatus);
-
+ connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::stepProgress, this, &FlameCreationTask::propogateStepProgress);
+ connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::details, this, &FlameCreationTask::setDetails);
m_mod_id_resolver->start();
loop.exec();
@@ -452,7 +457,7 @@ void FlameCreationTask::idResolverSucceeded(QEventLoop& loop)
void FlameCreationTask::setupDownloadJob(QEventLoop& loop)
{
- m_files_job.reset(new NetJob(tr("Mod download"), APPLICATION->network()));
+ m_files_job.reset(new NetJob(tr("Mod Download Flame"), APPLICATION->network()));
for (const auto& result : m_mod_id_resolver->getResults().files) {
QString filename = result.fileName;
if (!result.required) {
@@ -465,8 +470,9 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop)
switch (result.type) {
case Flame::File::Type::Folder: {
logWarning(tr("This 'Folder' may need extracting: %1").arg(relpath));
- // fall-through intentional, we treat these as plain old mods and dump them wherever.
+ // fallthrough intentional, we treat these as plain old mods and dump them wherever.
}
+ /* fallthrough */
case Flame::File::Type::SingleFile:
case Flame::File::Type::Mod: {
if (!result.url.isEmpty()) {
@@ -496,7 +502,11 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop)
m_files_job.reset();
setError(reason);
});
- connect(m_files_job.get(), &NetJob::progress, this, &FlameCreationTask::setProgress);
+ connect(m_files_job.get(), &NetJob::progress, this, [this](qint64 current, qint64 total){
+ setDetails(tr("%1 out of %2 complete").arg(current).arg(total));
+ setProgress(current, total);
+ });
+ connect(m_files_job.get(), &NetJob::stepProgress, this, &FlameCreationTask::propogateStepProgress);
connect(m_files_job.get(), &NetJob::finished, &loop, &QEventLoop::quit);
setStatus(tr("Downloading mods..."));
@@ -553,6 +563,8 @@ void FlameCreationTask::validateZIPResouces()
if (FS::move(localPath, destPath)) {
return destPath;
}
+ } else {
+ qDebug() << "Target folder of" << fileName << "is correct at" << targetFolder;
}
return localPath;
};
@@ -574,6 +586,9 @@ void FlameCreationTask::validateZIPResouces()
QString worldPath;
switch (type) {
+ case PackedResourceType::Mod :
+ validatePath(fileName, targetFolder, "mods");
+ break;
case PackedResourceType::ResourcePack :
validatePath(fileName, targetFolder, "resourcepacks");
break;
@@ -583,9 +598,6 @@ void FlameCreationTask::validateZIPResouces()
case PackedResourceType::DataPack :
validatePath(fileName, targetFolder, "datapacks");
break;
- case PackedResourceType::Mod :
- validatePath(fileName, targetFolder, "mods");
- break;
case PackedResourceType::ShaderPack :
// in theroy flame API can't do this but who knows, that *may* change ?
// better to handle it if it *does* occure in the future
diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp
index 7498e830..227ce489 100644
--- a/launcher/modplatform/flame/FlameModIndex.cpp
+++ b/launcher/modplatform/flame/FlameModIndex.cpp
@@ -39,15 +39,15 @@ void FlameMod::loadURLs(ModPlatform::IndexedPack& pack, QJsonObject& obj)
auto links_obj = Json::ensureObject(obj, "links");
pack.extraData.issuesUrl = Json::ensureString(links_obj, "issuesUrl");
- if(pack.extraData.issuesUrl.endsWith('/'))
+ if (pack.extraData.issuesUrl.endsWith('/'))
pack.extraData.issuesUrl.chop(1);
pack.extraData.sourceUrl = Json::ensureString(links_obj, "sourceUrl");
- if(pack.extraData.sourceUrl.endsWith('/'))
+ if (pack.extraData.sourceUrl.endsWith('/'))
pack.extraData.sourceUrl.chop(1);
pack.extraData.wikiUrl = Json::ensureString(links_obj, "wikiUrl");
- if(pack.extraData.wikiUrl.endsWith('/'))
+ if (pack.extraData.wikiUrl.endsWith('/'))
pack.extraData.wikiUrl.chop(1);
if (!pack.extraData.body.isEmpty())
@@ -56,7 +56,7 @@ void FlameMod::loadURLs(ModPlatform::IndexedPack& pack, QJsonObject& obj)
void FlameMod::loadBody(ModPlatform::IndexedPack& pack, QJsonObject& obj)
{
- pack.extraData.body = api.getModDescription(pack.addonId.toInt());
+ pack.extraData.body = api.getModDescription(pack.addonId.toInt());
if (!pack.extraData.issuesUrl.isEmpty() || !pack.extraData.sourceUrl.isEmpty() || !pack.extraData.wikiUrl.isEmpty())
pack.extraDataLoaded = true;
@@ -64,12 +64,12 @@ void FlameMod::loadBody(ModPlatform::IndexedPack& pack, QJsonObject& obj)
static QString enumToString(int hash_algorithm)
{
- switch(hash_algorithm){
- default:
- case 1:
- return "sha1";
- case 2:
- return "md5";
+ switch (hash_algorithm) {
+ default:
+ case 1:
+ return "sha1";
+ case 2:
+ return "md5";
}
}
@@ -84,12 +84,12 @@ void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
for (auto versionIter : arr) {
auto obj = versionIter.toObject();
-
+
auto file = loadIndexedPackVersion(obj);
- if(!file.addonId.isValid())
+ if (!file.addonId.isValid())
file.addonId = pack.addonId;
- if(file.fileId.isValid()) // Heuristic to check if the returned value is valid
+ if (file.fileId.isValid()) // Heuristic to check if the returned value is valid
unsortedVersions.append(file);
}
@@ -136,8 +136,61 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) ->
}
}
- if(load_changelog)
+ auto dependencies = Json::ensureArray(obj, "dependencies");
+ for (auto d : dependencies) {
+ auto dep = Json::ensureObject(d);
+ ModPlatform::Dependency dependency;
+ dependency.addonId = Json::requireInteger(dep, "modId");
+ switch (Json::requireInteger(dep, "relationType")) {
+ case 1: // EmbeddedLibrary
+ dependency.type = ModPlatform::DependencyType::EMBEDDED;
+ break;
+ case 2: // OptionalDependency
+ dependency.type = ModPlatform::DependencyType::OPTIONAL;
+ break;
+ case 3: // RequiredDependency
+ dependency.type = ModPlatform::DependencyType::REQUIRED;
+ break;
+ case 4: // Tool
+ dependency.type = ModPlatform::DependencyType::TOOL;
+ break;
+ case 5: // Incompatible
+ dependency.type = ModPlatform::DependencyType::INCOMPATIBLE;
+ break;
+ case 6: // Include
+ dependency.type = ModPlatform::DependencyType::INCLUDE;
+ break;
+ default:
+ dependency.type = ModPlatform::DependencyType::UNKNOWN;
+ break;
+ }
+ file.dependencies.append(dependency);
+ }
+
+ if (load_changelog)
file.changelog = api.getModFileChangelog(file.addonId.toInt(), file.fileId.toInt());
return file;
}
+
+ModPlatform::IndexedVersion FlameMod::loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr)
+{
+ QVector<ModPlatform::IndexedVersion> versions;
+ for (auto versionIter : arr) {
+ auto obj = versionIter.toObject();
+
+ auto file = loadIndexedPackVersion(obj);
+ if (!file.addonId.isValid())
+ file.addonId = m.addonId;
+
+ if (file.fileId.isValid()) // Heuristic to check if the returned value is valid
+ versions.append(file);
+ }
+
+ auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool {
+ // dates are in RFC 3339 format
+ return a.date > b.date;
+ };
+ std::sort(versions.begin(), versions.end(), orderSortPredicate);
+ return versions.front();
+};
diff --git a/launcher/modplatform/flame/FlameModIndex.h b/launcher/modplatform/flame/FlameModIndex.h
index 33c4a529..aa0d6f81 100644
--- a/launcher/modplatform/flame/FlameModIndex.h
+++ b/launcher/modplatform/flame/FlameModIndex.h
@@ -6,8 +6,8 @@
#include "modplatform/ModIndex.h"
-#include "BaseInstance.h"
#include <QNetworkAccessManager>
+#include "BaseInstance.h"
namespace FlameMod {
@@ -19,5 +19,5 @@ void loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
const shared_qobject_ptr<QNetworkAccessManager>& network,
const BaseInstance* inst);
auto loadIndexedPackVersion(QJsonObject& obj, bool load_changelog = false) -> ModPlatform::IndexedVersion;
-
-} // namespace FlameMod
+auto loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion;
+} // namespace FlameMod \ No newline at end of file
diff --git a/launcher/modplatform/flame/FlamePackExportTask.cpp b/launcher/modplatform/flame/FlamePackExportTask.cpp
new file mode 100644
index 00000000..ac0da214
--- /dev/null
+++ b/launcher/modplatform/flame/FlamePackExportTask.cpp
@@ -0,0 +1,473 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
+ * Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "FlamePackExportTask.h"
+#include <QJsonArray>
+#include <QJsonObject>
+
+#include <QCryptographicHash>
+#include <QFileInfo>
+#include <QMessageBox>
+#include <QtConcurrentRun>
+#include <algorithm>
+#include <memory>
+#include "Json.h"
+#include "MMCZip.h"
+#include "minecraft/PackProfile.h"
+#include "minecraft/mod/ModFolderModel.h"
+#include "modplatform/ModIndex.h"
+#include "modplatform/flame/FlameModIndex.h"
+#include "modplatform/helpers/HashUtils.h"
+#include "tasks/Task.h"
+
+const QString FlamePackExportTask::TEMPLATE = "<li><a href=\"{url}\">{name}{authors}</a></li>\n";
+const QStringList FlamePackExportTask::FILE_EXTENSIONS({ "jar", "zip" });
+
+FlamePackExportTask::FlamePackExportTask(const QString& name,
+ const QString& version,
+ const QString& author,
+ InstancePtr instance,
+ const QString& output,
+ MMCZip::FilterFunction filter)
+ : name(name)
+ , version(version)
+ , author(author)
+ , instance(instance)
+ , mcInstance(dynamic_cast<MinecraftInstance*>(instance.get()))
+ , gameRoot(instance->gameRoot())
+ , output(output)
+ , filter(filter)
+{}
+
+void FlamePackExportTask::executeTask()
+{
+ setStatus(tr("Searching for files..."));
+ setProgress(0, 5);
+ collectFiles();
+}
+
+bool FlamePackExportTask::abort()
+{
+ if (task != nullptr) {
+ task->abort();
+ task = nullptr;
+ emitAborted();
+ return true;
+ }
+
+ if (buildZipFuture.isRunning()) {
+ buildZipFuture.cancel();
+ // NOTE: Here we don't do `emitAborted()` because it will be done when `buildZipFuture` actually cancels, which may not occur
+ // immediately.
+ return true;
+ }
+
+ return false;
+}
+
+void FlamePackExportTask::collectFiles()
+{
+ setAbortable(false);
+ QCoreApplication::processEvents();
+
+ files.clear();
+ if (!MMCZip::collectFileListRecursively(instance->gameRoot(), nullptr, &files, filter)) {
+ emitFailed(tr("Could not search for files"));
+ return;
+ }
+
+ pendingHashes.clear();
+ resolvedFiles.clear();
+
+ if (mcInstance != nullptr) {
+ mcInstance->loaderModList()->update();
+ connect(mcInstance->loaderModList().get(), &ModFolderModel::updateFinished, this, &FlamePackExportTask::collectHashes);
+ } else
+ collectHashes();
+}
+
+void FlamePackExportTask::collectHashes()
+{
+ setAbortable(true);
+ setStatus(tr("Finding file hashes..."));
+ setProgress(1, 5);
+ auto allMods = mcInstance->loaderModList()->allMods();
+ ConcurrentTask::Ptr hashingTask(new ConcurrentTask(this, "MakeHashesTask", 10));
+ task.reset(hashingTask);
+ for (const QFileInfo& file : files) {
+ const QString relative = gameRoot.relativeFilePath(file.absoluteFilePath());
+ // require sensible file types
+ if (!std::any_of(FILE_EXTENSIONS.begin(), FILE_EXTENSIONS.end(), [&relative](const QString& extension) {
+ return relative.endsWith('.' + extension) || relative.endsWith('.' + extension + ".disabled");
+ }))
+ continue;
+
+ if (relative.startsWith("resourcepacks/") &&
+ (relative.endsWith(".zip") || relative.endsWith(".zip.disabled"))) { // is resourcepack
+ auto hashTask = Hashing::createFlameHasher(file.absoluteFilePath());
+ connect(hashTask.get(), &Hashing::Hasher::resultsReady, [this, relative, file](QString hash) {
+ if (m_state == Task::State::Running) {
+ pendingHashes.insert(hash, { relative, file.absoluteFilePath(), relative.endsWith(".zip") });
+ }
+ });
+ connect(hashTask.get(), &Task::failed, this, &FlamePackExportTask::emitFailed);
+ hashingTask->addTask(hashTask);
+ continue;
+ }
+
+ if (auto modIter = std::find_if(allMods.begin(), allMods.end(), [&file](Mod* mod) { return mod->fileinfo() == file; });
+ modIter != allMods.end()) {
+ const Mod* mod = *modIter;
+ if (!mod || mod->type() == ResourceType::FOLDER) {
+ continue;
+ }
+ if (mod->metadata() && mod->metadata()->provider == ModPlatform::ResourceProvider::FLAME) {
+ resolvedFiles.insert(mod->fileinfo().absoluteFilePath(),
+ { mod->metadata()->project_id.toInt(), mod->metadata()->file_id.toInt(), mod->enabled(), true,
+ mod->metadata()->name, mod->metadata()->slug, mod->authors().join(", ") });
+ continue;
+ }
+
+ auto hashTask = Hashing::createFlameHasher(mod->fileinfo().absoluteFilePath());
+ connect(hashTask.get(), &Hashing::Hasher::resultsReady, [this, mod](QString hash) {
+ if (m_state == Task::State::Running) {
+ pendingHashes.insert(hash, { mod->name(), mod->fileinfo().absoluteFilePath(), mod->enabled(), true });
+ }
+ });
+ connect(hashTask.get(), &Task::failed, this, &FlamePackExportTask::emitFailed);
+ hashingTask->addTask(hashTask);
+ }
+ }
+ auto progressStep = std::make_shared<TaskStepProgress>();
+ connect(hashingTask.get(), &Task::finished, this, [this, progressStep] {
+ progressStep->state = TaskStepState::Succeeded;
+ stepProgress(*progressStep);
+ });
+
+ connect(hashingTask.get(), &Task::succeeded, this, &FlamePackExportTask::makeApiRequest);
+ connect(hashingTask.get(), &Task::failed, this, [this, progressStep](QString reason) {
+ progressStep->state = TaskStepState::Failed;
+ stepProgress(*progressStep);
+ emitFailed(reason);
+ });
+ connect(hashingTask.get(), &Task::stepProgress, this, &FlamePackExportTask::propogateStepProgress);
+
+ connect(hashingTask.get(), &Task::progress, this, [this, progressStep](qint64 current, qint64 total) {
+ progressStep->update(current, total);
+ stepProgress(*progressStep);
+ });
+ connect(hashingTask.get(), &Task::status, this, [this, progressStep](QString status) {
+ progressStep->status = status;
+ stepProgress(*progressStep);
+ });
+ hashingTask->start();
+}
+
+void FlamePackExportTask::makeApiRequest()
+{
+ if (pendingHashes.isEmpty()) {
+ buildZip();
+ return;
+ }
+
+ setStatus(tr("Finding versions for hashes..."));
+ setProgress(2, 5);
+ auto response = std::make_shared<QByteArray>();
+
+ QList<uint> fingerprints;
+ for (auto& murmur : pendingHashes.keys()) {
+ fingerprints.push_back(murmur.toUInt());
+ }
+
+ task.reset(api.matchFingerprints(fingerprints, response));
+
+ connect(task.get(), &Task::succeeded, this, [this, response] {
+ QJsonParseError parseError{};
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &parseError);
+ if (parseError.error != QJsonParseError::NoError) {
+ qWarning() << "Error while parsing JSON response from CurseForge::CurrentVersions at " << parseError.offset
+ << " reason: " << parseError.errorString();
+ qWarning() << *response;
+
+ failed(parseError.errorString());
+ return;
+ }
+
+ try {
+ auto docObj = Json::requireObject(doc);
+ auto dataObj = Json::requireObject(docObj, "data");
+ auto dataArr = Json::requireArray(dataObj, "exactMatches");
+
+ if (dataArr.isEmpty()) {
+ qWarning() << "No matches found for fingerprint search!";
+
+ return;
+ }
+ for (auto match : dataArr) {
+ auto matchObj = Json::ensureObject(match, {});
+ auto fileObj = Json::ensureObject(matchObj, "file", {});
+
+ if (matchObj.isEmpty() || fileObj.isEmpty()) {
+ qWarning() << "Fingerprint match is empty!";
+
+ return;
+ }
+
+ auto fingerprint = QString::number(Json::ensureVariant(fileObj, "fileFingerprint").toUInt());
+ auto mod = pendingHashes.find(fingerprint);
+ if (mod == pendingHashes.end()) {
+ qWarning() << "Invalid fingerprint from the API response.";
+ continue;
+ }
+
+ setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(mod->name));
+ if (Json::ensureBoolean(fileObj, "isAvailable", false, "isAvailable"))
+ resolvedFiles.insert(mod->path, { Json::requireInteger(fileObj, "modId"), Json::requireInteger(fileObj, "id"),
+ mod->enabled, mod->isMod });
+ }
+
+ } catch (Json::JsonException& e) {
+ qDebug() << e.cause();
+ qDebug() << doc;
+ }
+ pendingHashes.clear();
+ });
+ connect(task.get(), &Task::finished, this, &FlamePackExportTask::getProjectsInfo);
+ connect(task.get(), &NetJob::failed, this, &FlamePackExportTask::emitFailed);
+ task->start();
+}
+
+void FlamePackExportTask::getProjectsInfo()
+{
+ setStatus(tr("Finding project info from CurseForge..."));
+ setProgress(3, 5);
+ QStringList addonIds;
+ for (const auto& resolved : resolvedFiles) {
+ if (resolved.slug.isEmpty()) {
+ addonIds << QString::number(resolved.addonId);
+ }
+ }
+
+ auto response = std::make_shared<QByteArray>();
+ Task::Ptr projTask;
+
+ if (addonIds.isEmpty()) {
+ buildZip();
+ return;
+ } else if (addonIds.size() == 1) {
+ projTask = api.getProject(*addonIds.begin(), response);
+ } else {
+ projTask = api.getProjects(addonIds, response);
+ }
+
+ connect(projTask.get(), &Task::succeeded, this, [this, response, addonIds] {
+ QJsonParseError parseError{};
+ auto doc = QJsonDocument::fromJson(*response, &parseError);
+ if (parseError.error != QJsonParseError::NoError) {
+ qWarning() << "Error while parsing JSON response from CurseForge projects task at " << parseError.offset
+ << " reason: " << parseError.errorString();
+ qWarning() << *response;
+ failed(parseError.errorString());
+ return;
+ }
+
+ try {
+ QJsonArray entries;
+ if (addonIds.size() == 1)
+ entries = { Json::requireObject(Json::requireObject(doc), "data") };
+ else
+ entries = Json::requireArray(Json::requireObject(doc), "data");
+
+ for (auto entry : entries) {
+ auto entryObj = Json::requireObject(entry);
+
+ try {
+ setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(Json::requireString(entryObj, "name")));
+
+ ModPlatform::IndexedPack pack;
+ FlameMod::loadIndexedPack(pack, entryObj);
+ for (auto key : resolvedFiles.keys()) {
+ auto val = resolvedFiles.value(key);
+ if (val.addonId == pack.addonId) {
+ val.name = pack.name;
+ val.slug = pack.slug;
+ QStringList authors;
+ for (auto author : pack.authors)
+ authors << author.name;
+
+ val.authors = authors.join(", ");
+ resolvedFiles[key] = val;
+ }
+ }
+
+ } catch (Json::JsonException& e) {
+ qDebug() << e.cause();
+ qDebug() << entries;
+ }
+ }
+ } catch (Json::JsonException& e) {
+ qDebug() << e.cause();
+ qDebug() << doc;
+ }
+ buildZip();
+ });
+ task.reset(projTask);
+ task->start();
+}
+
+void FlamePackExportTask::buildZip()
+{
+ setStatus(tr("Adding files..."));
+ setProgress(4, 5);
+
+ buildZipFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this]() {
+ QuaZip zip(output);
+ if (!zip.open(QuaZip::mdCreate)) {
+ QFile::remove(output);
+ return BuildZipResult(tr("Could not create file"));
+ }
+
+ if (buildZipFuture.isCanceled())
+ return BuildZipResult();
+
+ QuaZipFile indexFile(&zip);
+ if (!indexFile.open(QIODevice::WriteOnly, QuaZipNewInfo("manifest.json"))) {
+ QFile::remove(output);
+ return BuildZipResult(tr("Could not create index"));
+ }
+ indexFile.write(generateIndex());
+
+ QuaZipFile modlist(&zip);
+ if (!modlist.open(QIODevice::WriteOnly, QuaZipNewInfo("modlist.html"))) {
+ QFile::remove(output);
+ return BuildZipResult(tr("Could not create index"));
+ }
+ QString content = "";
+ for (auto mod : resolvedFiles) {
+ if (mod.isMod) {
+ content += QString(TEMPLATE)
+ .replace("{name}", mod.name.toHtmlEscaped())
+ .replace("{url}", ModPlatform::getMetaURL(ModPlatform::ResourceProvider::FLAME, mod.addonId).toHtmlEscaped())
+ .replace("{authors}", !mod.authors.isEmpty() ? QString(" (by %1)").arg(mod.authors).toHtmlEscaped() : "");
+ }
+ }
+ content = "<ul>" + content + "</ul>";
+ modlist.write(content.toUtf8());
+
+ auto progressStep = std::make_shared<TaskStepProgress>();
+
+ size_t progress = 0;
+ for (const QFileInfo& file : files) {
+ if (buildZipFuture.isCanceled()) {
+ QFile::remove(output);
+ progressStep->state = TaskStepState::Failed;
+ stepProgress(*progressStep);
+ return BuildZipResult();
+ }
+ progressStep->update(progress, files.length());
+ stepProgress(*progressStep);
+
+ const QString relative = gameRoot.relativeFilePath(file.absoluteFilePath());
+ if (!resolvedFiles.contains(file.absoluteFilePath()) &&
+ !JlCompress::compressFile(&zip, file.absoluteFilePath(), "overrides/" + relative)) {
+ QFile::remove(output);
+ return BuildZipResult(tr("Could not read and compress %1").arg(relative));
+ }
+ progress++;
+ }
+
+ zip.close();
+
+ if (zip.getZipError() != 0) {
+ QFile::remove(output);
+ progressStep->state = TaskStepState::Failed;
+ stepProgress(*progressStep);
+ return BuildZipResult(tr("A zip error occurred"));
+ }
+ progressStep->state = TaskStepState::Succeeded;
+ stepProgress(*progressStep);
+ return BuildZipResult();
+ });
+ connect(&buildZipWatcher, &QFutureWatcher<BuildZipResult>::finished, this, &FlamePackExportTask::finish);
+ buildZipWatcher.setFuture(buildZipFuture);
+}
+
+void FlamePackExportTask::finish()
+{
+ if (buildZipFuture.isCanceled())
+ emitAborted();
+ else {
+ const BuildZipResult result = buildZipFuture.result();
+ if (result.has_value())
+ emitFailed(result.value());
+ else
+ emitSucceeded();
+ }
+}
+
+QByteArray FlamePackExportTask::generateIndex()
+{
+ QJsonObject obj;
+ obj["manifestType"] = "minecraftModpack";
+ obj["manifestVersion"] = 1;
+ obj["name"] = name;
+ obj["version"] = version;
+ obj["author"] = author;
+ obj["overrides"] = "overrides";
+ if (mcInstance) {
+ QJsonObject version;
+ auto profile = mcInstance->getPackProfile();
+ // collect all supported components
+ const ComponentPtr minecraft = profile->getComponent("net.minecraft");
+ const ComponentPtr quilt = profile->getComponent("org.quiltmc.quilt-loader");
+ const ComponentPtr fabric = profile->getComponent("net.fabricmc.fabric-loader");
+ const ComponentPtr forge = profile->getComponent("net.minecraftforge");
+
+ // convert all available components to mrpack dependencies
+ if (minecraft != nullptr)
+ version["version"] = minecraft->m_version;
+ QString id;
+ if (quilt != nullptr)
+ id = "quilt-" + quilt->getVersion();
+ else if (fabric != nullptr)
+ id = "fabric-" + fabric->getVersion();
+ else if (forge != nullptr)
+ id = "forge-" + forge->getVersion();
+ version["modLoaders"] = QJsonArray();
+ if (!id.isEmpty()) {
+ QJsonObject loader;
+ loader["id"] = id;
+ loader["primary"] = true;
+ version["modLoaders"] = QJsonArray({ loader });
+ }
+ obj["minecraft"] = version;
+ }
+
+ QJsonArray files;
+ for (auto mod : resolvedFiles) {
+ QJsonObject file;
+ file["projectID"] = mod.addonId;
+ file["fileID"] = mod.version;
+ file["required"] = mod.enabled;
+ files << file;
+ }
+ obj["files"] = files;
+
+ return QJsonDocument(obj).toJson(QJsonDocument::Compact);
+}
diff --git a/launcher/modplatform/flame/FlamePackExportTask.h b/launcher/modplatform/flame/FlamePackExportTask.h
new file mode 100644
index 00000000..3dee0a7e
--- /dev/null
+++ b/launcher/modplatform/flame/FlamePackExportTask.h
@@ -0,0 +1,90 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
+ * Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <QFuture>
+#include <QFutureWatcher>
+#include "BaseInstance.h"
+#include "MMCZip.h"
+#include "minecraft/MinecraftInstance.h"
+#include "modplatform/flame/FlameAPI.h"
+#include "tasks/Task.h"
+
+class FlamePackExportTask : public Task {
+ public:
+ FlamePackExportTask(const QString& name,
+ const QString& version,
+ const QString& author,
+ InstancePtr instance,
+ const QString& output,
+ MMCZip::FilterFunction filter);
+
+ protected:
+ void executeTask() override;
+ bool abort() override;
+
+ private:
+ static const QString TEMPLATE;
+ static const QStringList FILE_EXTENSIONS;
+
+ // inputs
+ const QString name, version, author;
+ const InstancePtr instance;
+ MinecraftInstance* mcInstance;
+ const QDir gameRoot;
+ const QString output;
+ const MMCZip::FilterFunction filter;
+
+ typedef std::optional<QString> BuildZipResult;
+ struct ResolvedFile {
+ int addonId;
+ int version;
+ bool enabled;
+ bool isMod;
+
+ QString name;
+ QString slug;
+ QString authors;
+ };
+ struct HashInfo {
+ QString name;
+ QString path;
+ bool enabled;
+ bool isMod;
+ };
+
+ FlameAPI api;
+
+ QFileInfoList files;
+ QMap<QString, HashInfo> pendingHashes{};
+ QMap<QString, ResolvedFile> resolvedFiles{};
+ Task::Ptr task;
+ QFuture<BuildZipResult> buildZipFuture;
+ QFutureWatcher<BuildZipResult> buildZipWatcher;
+
+ void collectFiles();
+ void collectHashes();
+ void makeApiRequest();
+ void getProjectsInfo();
+ void buildZip();
+ void finish();
+
+ QByteArray generateIndex();
+};
diff --git a/launcher/modplatform/flame/FlamePackIndex.h b/launcher/modplatform/flame/FlamePackIndex.h
index 1ca0fc0e..b089b722 100644
--- a/launcher/modplatform/flame/FlamePackIndex.h
+++ b/launcher/modplatform/flame/FlamePackIndex.h
@@ -4,6 +4,7 @@
#include <QMetaType>
#include <QString>
#include <QVector>
+#include "modplatform/ModIndex.h"
namespace Flame {
@@ -27,8 +28,7 @@ struct ModpackExtra {
QString sourceUrl;
};
-struct IndexedPack
-{
+struct IndexedPack {
int addonId;
QString name;
QString description;
@@ -43,9 +43,9 @@ struct IndexedPack
ModpackExtra extra;
};
-void loadIndexedPack(IndexedPack & m, QJsonObject & obj);
+void loadIndexedPack(IndexedPack& m, QJsonObject& obj);
void loadIndexedInfo(IndexedPack&, QJsonObject&);
-void loadIndexedPackVersions(IndexedPack & m, QJsonArray & arr);
-}
+void loadIndexedPackVersions(IndexedPack& m, QJsonArray& arr);
+} // namespace Flame
Q_DECLARE_METATYPE(Flame::IndexedPack)
diff --git a/launcher/modplatform/flame/PackManifest.cpp b/launcher/modplatform/flame/PackManifest.cpp
index 22008297..ee4d0766 100644
--- a/launcher/modplatform/flame/PackManifest.cpp
+++ b/launcher/modplatform/flame/PackManifest.cpp
@@ -76,13 +76,8 @@ bool Flame::File::parseFromObject(const QJsonObject& obj, bool throw_on_blocked
// It is also optional
type = File::Type::SingleFile;
- if (fileName.endsWith(".zip")) {
- // this is probably a resource pack
- targetFolder = "resourcepacks";
- } else {
- // this is probably a mod, dunno what else could modpacks download
- targetFolder = "mods";
- }
+ targetFolder = "mods";
+
// get the hash
hash = QString();
auto hashes = Json::ensureArray(obj, "hashes");
diff --git a/launcher/modplatform/helpers/ExportToModList.cpp b/launcher/modplatform/helpers/ExportToModList.cpp
new file mode 100644
index 00000000..1f01c4a8
--- /dev/null
+++ b/launcher/modplatform/helpers/ExportToModList.cpp
@@ -0,0 +1,200 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+#include "ExportToModList.h"
+#include <QJsonArray>
+#include <QJsonDocument>
+#include <QJsonObject>
+
+namespace ExportToModList {
+QString toHTML(QList<Mod*> mods, OptionalData extraData)
+{
+ QStringList lines;
+ for (auto mod : mods) {
+ auto meta = mod->metadata();
+ auto modName = mod->name().toHtmlEscaped();
+ if (extraData & Url) {
+ auto url = mod->metaurl().toHtmlEscaped();
+ if (!url.isEmpty())
+ modName = QString("<a href=\"%1\">%2</a>").arg(url, modName);
+ }
+ auto line = modName;
+ if (extraData & Version) {
+ auto ver = mod->version();
+ if (ver.isEmpty() && meta != nullptr)
+ ver = meta->version().toString();
+ if (!ver.isEmpty())
+ line += QString(" [%1]").arg(ver.toHtmlEscaped());
+ }
+ if (extraData & Authors && !mod->authors().isEmpty())
+ line += " by " + mod->authors().join(", ").toHtmlEscaped();
+ lines.append(QString("<li>%1</li>").arg(line));
+ }
+ return QString("<html><body><ul>\n\t%1\n</ul></body></html>").arg(lines.join("\n\t"));
+}
+
+QString toMarkdown(QList<Mod*> mods, OptionalData extraData)
+{
+ QStringList lines;
+ for (auto mod : mods) {
+ auto meta = mod->metadata();
+ auto modName = mod->name();
+ if (extraData & Url) {
+ auto url = mod->metaurl();
+ if (!url.isEmpty())
+ modName = QString("[%1](%2)").arg(modName, url);
+ }
+ auto line = modName;
+ if (extraData & Version) {
+ auto ver = mod->version();
+ if (ver.isEmpty() && meta != nullptr)
+ ver = meta->version().toString();
+ if (!ver.isEmpty())
+ line += QString(" [%1]").arg(ver);
+ }
+ if (extraData & Authors && !mod->authors().isEmpty())
+ line += " by " + mod->authors().join(", ");
+ lines << "- " + line;
+ }
+ return lines.join("\n");
+}
+
+QString toPlainTXT(QList<Mod*> mods, OptionalData extraData)
+{
+ QStringList lines;
+ for (auto mod : mods) {
+ auto meta = mod->metadata();
+ auto modName = mod->name();
+
+ auto line = modName;
+ if (extraData & Url) {
+ auto url = mod->metaurl();
+ if (!url.isEmpty())
+ line += QString(" (%1)").arg(url);
+ }
+ if (extraData & Version) {
+ auto ver = mod->version();
+ if (ver.isEmpty() && meta != nullptr)
+ ver = meta->version().toString();
+ if (!ver.isEmpty())
+ line += QString(" [%1]").arg(ver);
+ }
+ if (extraData & Authors && !mod->authors().isEmpty())
+ line += " by " + mod->authors().join(", ");
+ lines << line;
+ }
+ return lines.join("\n");
+}
+
+QString toJSON(QList<Mod*> mods, OptionalData extraData)
+{
+ QJsonArray lines;
+ for (auto mod : mods) {
+ auto meta = mod->metadata();
+ auto modName = mod->name();
+ QJsonObject line;
+ line["name"] = modName;
+ if (extraData & Url) {
+ auto url = mod->metaurl();
+ if (!url.isEmpty())
+ line["url"] = url;
+ }
+ if (extraData & Version) {
+ auto ver = mod->version();
+ if (ver.isEmpty() && meta != nullptr)
+ ver = meta->version().toString();
+ if (!ver.isEmpty())
+ line["version"] = ver;
+ }
+ if (extraData & Authors && !mod->authors().isEmpty())
+ line["authors"] = QJsonArray::fromStringList(mod->authors());
+ lines << line;
+ }
+ QJsonDocument doc;
+ doc.setArray(lines);
+ return doc.toJson();
+}
+
+QString toCSV(QList<Mod*> mods, OptionalData extraData)
+{
+ QStringList lines;
+ for (auto mod : mods) {
+ QStringList data;
+ auto meta = mod->metadata();
+ auto modName = mod->name();
+
+ data << modName;
+ if (extraData & Url)
+ data << mod->metaurl();
+ if (extraData & Version) {
+ auto ver = mod->version();
+ if (ver.isEmpty() && meta != nullptr)
+ ver = meta->version().toString();
+ data << ver;
+ }
+ if (extraData & Authors) {
+ QString authors;
+ if (mod->authors().length() == 1)
+ authors = mod->authors().back();
+ else if (mod->authors().length() > 1)
+ authors = QString("\"%1\"").arg(mod->authors().join(","));
+ data << authors;
+ }
+ lines << data.join(",");
+ }
+ return lines.join("\n");
+}
+
+QString exportToModList(QList<Mod*> mods, Formats format, OptionalData extraData)
+{
+ switch (format) {
+ case HTML:
+ return toHTML(mods, extraData);
+ case MARKDOWN:
+ return toMarkdown(mods, extraData);
+ case PLAINTXT:
+ return toPlainTXT(mods, extraData);
+ case JSON:
+ return toJSON(mods, extraData);
+ case CSV:
+ return toCSV(mods, extraData);
+ default: {
+ return QString("unknown format:%1").arg(format);
+ }
+ }
+}
+
+QString exportToModList(QList<Mod*> mods, QString lineTemplate)
+{
+ QStringList lines;
+ for (auto mod : mods) {
+ auto meta = mod->metadata();
+ auto modName = mod->name();
+ auto url = mod->metaurl();
+ auto ver = mod->version();
+ if (ver.isEmpty() && meta != nullptr)
+ ver = meta->version().toString();
+ auto authors = mod->authors().join(", ");
+ lines << QString(lineTemplate)
+ .replace("{name}", modName)
+ .replace("{url}", url)
+ .replace("{version}", ver)
+ .replace("{authors}", authors);
+ }
+ return lines.join("\n");
+}
+} // namespace ExportToModList \ No newline at end of file
diff --git a/launcher/modplatform/helpers/ExportToModList.h b/launcher/modplatform/helpers/ExportToModList.h
new file mode 100644
index 00000000..7ea4ba9c
--- /dev/null
+++ b/launcher/modplatform/helpers/ExportToModList.h
@@ -0,0 +1,33 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+#pragma once
+#include <QList>
+#include <QString>
+#include "minecraft/mod/Mod.h"
+
+namespace ExportToModList {
+
+enum Formats { HTML, MARKDOWN, PLAINTXT, JSON, CSV, CUSTOM };
+enum OptionalData {
+ Authors = 1 << 0,
+ Url = 1 << 1,
+ Version = 1 << 2,
+};
+QString exportToModList(QList<Mod*> mods, Formats format, OptionalData extraData);
+QString exportToModList(QList<Mod*> mods, QString lineTemplate);
+} // namespace ExportToModList
diff --git a/launcher/modplatform/helpers/HashUtils.cpp b/launcher/modplatform/helpers/HashUtils.cpp
index 81c94e1b..6ff1d171 100644
--- a/launcher/modplatform/helpers/HashUtils.cpp
+++ b/launcher/modplatform/helpers/HashUtils.cpp
@@ -71,6 +71,7 @@ void ModrinthHasher::executeTask()
emitFailed("Empty hash!");
} else {
emitSucceeded();
+ emit resultsReady(m_hash);
}
}
@@ -88,13 +89,13 @@ void FlameHasher::executeTask()
emitFailed("Empty hash!");
} else {
emitSucceeded();
+ emit resultsReady(m_hash);
}
}
-
-BlockedModHasher::BlockedModHasher(QString file_path, ModPlatform::ResourceProvider provider)
- : Hasher(file_path), provider(provider) {
- setObjectName(QString("BlockedModHasher: %1").arg(file_path));
+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();
}
@@ -120,14 +121,17 @@ void BlockedModHasher::executeTask()
emitFailed("Empty hash!");
} else {
emitSucceeded();
+ emit resultsReady(m_hash);
}
}
-QStringList BlockedModHasher::getHashTypes() {
+QStringList BlockedModHasher::getHashTypes()
+{
return ProviderCaps.hashType(provider);
}
-bool BlockedModHasher::useHashType(QString type) {
+bool BlockedModHasher::useHashType(QString type)
+{
auto types = ProviderCaps.hashType(provider);
if (types.contains(type)) {
hash_type = type;
diff --git a/launcher/modplatform/helpers/HashUtils.h b/launcher/modplatform/helpers/HashUtils.h
index 91146a52..73a2435a 100644
--- a/launcher/modplatform/helpers/HashUtils.h
+++ b/launcher/modplatform/helpers/HashUtils.h
@@ -8,6 +8,7 @@
namespace Hashing {
class Hasher : public Task {
+ Q_OBJECT
public:
using Ptr = shared_qobject_ptr<Hasher>;
@@ -21,6 +22,9 @@ class Hasher : public Task {
QString getResult() const { return m_hash; };
QString getPath() const { return m_path; };
+ signals:
+ void resultsReady(QString hash);
+
protected:
QString m_hash;
QString m_path;
@@ -48,6 +52,7 @@ class BlockedModHasher : public Hasher {
QStringList getHashTypes();
bool useHashType(QString type);
+
private:
ModPlatform::ResourceProvider provider;
QString hash_type;
diff --git a/launcher/modplatform/helpers/NetworkResourceAPI.cpp b/launcher/modplatform/helpers/NetworkResourceAPI.cpp
index 010ac15e..c278f800 100644
--- a/launcher/modplatform/helpers/NetworkResourceAPI.cpp
+++ b/launcher/modplatform/helpers/NetworkResourceAPI.cpp
@@ -3,6 +3,7 @@
// SPDX-License-Identifier: GPL-3.0-only
#include "NetworkResourceAPI.h"
+#include <memory>
#include "Application.h"
#include "net/NetJob.h"
@@ -19,12 +20,12 @@ Task::Ptr NetworkResourceAPI::searchProjects(SearchArgs&& args, SearchCallbacks&
auto search_url = search_url_optional.value();
- auto response = new QByteArray();
+ auto response = std::make_shared<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, [=]{
+ QObject::connect(netJob.get(), &NetJob::succeeded, [this, response, callbacks] {
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
@@ -40,23 +41,21 @@ Task::Ptr NetworkResourceAPI::searchProjects(SearchArgs&& args, SearchCallbacks&
callbacks.on_succeed(doc);
});
- QObject::connect(netJob.get(), &NetJob::failed, [=](QString reason){
+ QObject::connect(netJob.get(), &NetJob::failed, [&netJob, callbacks](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();
+ callbacks.on_fail(reason, network_error_code);
});
+ QObject::connect(netJob.get(), &NetJob::aborted, [callbacks] { callbacks.on_abort(); });
return netJob;
}
Task::Ptr NetworkResourceAPI::getProjectInfo(ProjectInfoArgs&& args, ProjectInfoCallbacks&& callbacks) const
{
- auto response = new QByteArray();
+ auto response = std::make_shared<QByteArray>();
auto job = getProject(args.pack.addonId.toString(), response);
QObject::connect(job.get(), &NetJob::succeeded, [response, callbacks, args] {
@@ -84,11 +83,11 @@ Task::Ptr NetworkResourceAPI::getProjectVersions(VersionSearchArgs&& args, Versi
auto versions_url = versions_url_optional.value();
auto netJob = makeShared<NetJob>(QString("%1::Versions").arg(args.pack.name), APPLICATION->network());
- auto response = new QByteArray();
+ auto response = std::make_shared<QByteArray>();
netJob->addNetAction(Net::Download::makeByteArray(versions_url, response));
- QObject::connect(netJob.get(), &NetJob::succeeded, [=] {
+ QObject::connect(netJob.get(), &NetJob::succeeded, [response, callbacks, args] {
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
@@ -101,14 +100,10 @@ Task::Ptr NetworkResourceAPI::getProjectVersions(VersionSearchArgs&& args, Versi
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
+Task::Ptr NetworkResourceAPI::getProject(QString addonId, std::shared_ptr<QByteArray> response) const
{
auto project_url_optional = getInfoURL(addonId);
if (!project_url_optional.has_value())
@@ -120,9 +115,34 @@ Task::Ptr NetworkResourceAPI::getProject(QString addonId, QByteArray* response)
netJob->addNetAction(Net::Download::makeByteArray(QUrl(project_url), response));
- QObject::connect(netJob.get(), &NetJob::finished, [response] {
- delete response;
+ return netJob;
+}
+
+Task::Ptr NetworkResourceAPI::getDependencyVersion(DependencySearchArgs&& args, DependencySearchCallbacks&& callbacks) const
+{
+ auto versions_url_optional = getDependencyURL(args);
+ if (!versions_url_optional.has_value())
+ return nullptr;
+
+ auto versions_url = versions_url_optional.value();
+
+ auto netJob = makeShared<NetJob>(QString("%1::Dependency").arg(args.dependency.addonId.toString()), APPLICATION->network());
+ auto response = std::make_shared<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.dependency);
});
return netJob;
-}
+};
diff --git a/launcher/modplatform/helpers/NetworkResourceAPI.h b/launcher/modplatform/helpers/NetworkResourceAPI.h
index 94813bec..b72e8253 100644
--- a/launcher/modplatform/helpers/NetworkResourceAPI.h
+++ b/launcher/modplatform/helpers/NetworkResourceAPI.h
@@ -4,19 +4,22 @@
#pragma once
+#include <memory>
#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 getProject(QString addonId, std::shared_ptr<QByteArray> response) const override;
Task::Ptr getProjectInfo(ProjectInfoArgs&&, ProjectInfoCallbacks&&) const override;
Task::Ptr getProjectVersions(VersionSearchArgs&&, VersionSearchCallbacks&&) const override;
+ Task::Ptr getDependencyVersion(DependencySearchArgs&&, DependencySearchCallbacks&&) 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;
+ [[nodiscard]] virtual auto getDependencyURL(DependencySearchArgs const& args) const -> std::optional<QString> = 0;
};
diff --git a/launcher/modplatform/legacy_ftb/PackFetchTask.cpp b/launcher/modplatform/legacy_ftb/PackFetchTask.cpp
index e8768c5c..a8a0fc2c 100644
--- a/launcher/modplatform/legacy_ftb/PackFetchTask.cpp
+++ b/launcher/modplatform/legacy_ftb/PackFetchTask.cpp
@@ -51,11 +51,11 @@ void PackFetchTask::fetch()
QUrl publicPacksUrl = QUrl(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/modpacks.xml");
qDebug() << "Downloading public version info from" << publicPacksUrl.toString();
- jobPtr->addNetAction(Net::Download::makeByteArray(publicPacksUrl, &publicModpacksXmlFileData));
+ jobPtr->addNetAction(Net::Download::makeByteArray(publicPacksUrl, publicModpacksXmlFileData));
QUrl thirdPartyUrl = QUrl(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/thirdparty.xml");
qDebug() << "Downloading thirdparty version info from" << thirdPartyUrl.toString();
- jobPtr->addNetAction(Net::Download::makeByteArray(thirdPartyUrl, &thirdPartyModpacksXmlFileData));
+ jobPtr->addNetAction(Net::Download::makeByteArray(thirdPartyUrl, thirdPartyModpacksXmlFileData));
QObject::connect(jobPtr.get(), &NetJob::succeeded, this, &PackFetchTask::fileDownloadFinished);
QObject::connect(jobPtr.get(), &NetJob::failed, this, &PackFetchTask::fileDownloadFailed);
@@ -64,22 +64,19 @@ void PackFetchTask::fetch()
jobPtr->start();
}
-void PackFetchTask::fetchPrivate(const QStringList & toFetch)
+void PackFetchTask::fetchPrivate(const QStringList& toFetch)
{
QString privatePackBaseUrl = BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/%1.xml";
- for (auto &packCode: toFetch)
- {
- QByteArray *data = new QByteArray();
- NetJob *job = new NetJob("Fetching private pack", m_network);
+ for (auto& packCode : toFetch) {
+ auto data = std::make_shared<QByteArray>();
+ NetJob* job = new NetJob("Fetching private pack", m_network);
job->addNetAction(Net::Download::makeByteArray(privatePackBaseUrl.arg(packCode), data));
- QObject::connect(job, &NetJob::succeeded, this, [this, job, data, packCode]
- {
+ QObject::connect(job, &NetJob::succeeded, this, [this, job, data, packCode] {
ModpackList packs;
parseAndAddPacks(*data, PackType::Private, packs);
- foreach(Modpack currentPack, packs)
- {
+ foreach (Modpack currentPack, packs) {
currentPack.packCode = packCode;
emit privateFileDownloadFinished(currentPack);
}
@@ -87,24 +84,20 @@ void PackFetchTask::fetchPrivate(const QStringList & toFetch)
job->deleteLater();
data->clear();
- delete data;
});
- QObject::connect(job, &NetJob::failed, this, [this, job, packCode, data](QString reason)
- {
+ QObject::connect(job, &NetJob::failed, this, [this, job, packCode, data](QString reason) {
emit privateFileDownloadFailed(reason, packCode);
job->deleteLater();
data->clear();
- delete data;
});
- QObject::connect(job, &NetJob::aborted, this, [this, job, data]{
+ QObject::connect(job, &NetJob::aborted, this, [this, job, data] {
emit aborted();
job->deleteLater();
data->clear();
- delete data;
});
job->start();
@@ -117,27 +110,22 @@ void PackFetchTask::fileDownloadFinished()
QStringList failedLists;
- if(!parseAndAddPacks(publicModpacksXmlFileData, PackType::Public, publicPacks))
- {
+ if (!parseAndAddPacks(*publicModpacksXmlFileData, PackType::Public, publicPacks)) {
failedLists.append(tr("Public Packs"));
}
- if(!parseAndAddPacks(thirdPartyModpacksXmlFileData, PackType::ThirdParty, thirdPartyPacks))
- {
+ if (!parseAndAddPacks(*thirdPartyModpacksXmlFileData, PackType::ThirdParty, thirdPartyPacks)) {
failedLists.append(tr("Third Party Packs"));
}
- if(failedLists.size() > 0)
- {
+ if (failedLists.size() > 0) {
emit failed(tr("Failed to download some pack lists: %1").arg(failedLists.join("\n- ")));
- }
- else
- {
+ } else {
emit finished(publicPacks, thirdPartyPacks);
}
}
-bool PackFetchTask::parseAndAddPacks(QByteArray &data, PackType packType, ModpackList &list)
+bool PackFetchTask::parseAndAddPacks(QByteArray& data, PackType packType, ModpackList& list)
{
QDomDocument doc;
@@ -145,8 +133,7 @@ bool PackFetchTask::parseAndAddPacks(QByteArray &data, PackType packType, Modpac
int errorLine = -1;
int errorCol = -1;
- if(!doc.setContent(data, false, &errorMsg, &errorLine, &errorCol))
- {
+ if (!doc.setContent(data, false, &errorMsg, &errorLine, &errorCol)) {
auto fullErrMsg = QString("Failed to fetch modpack data: %1 %2:%3!").arg(errorMsg).arg(errorLine).arg(errorCol);
qWarning() << fullErrMsg;
data.clear();
@@ -154,8 +141,7 @@ bool PackFetchTask::parseAndAddPacks(QByteArray &data, PackType packType, Modpac
}
QDomNodeList nodes = doc.elementsByTagName("modpack");
- for(int i = 0; i < nodes.length(); i++)
- {
+ for (int i = 0; i < nodes.length(); i++) {
QDomElement element = nodes.at(i).toElement();
Modpack modpack;
@@ -169,26 +155,20 @@ bool PackFetchTask::parseAndAddPacks(QByteArray &data, PackType packType, Modpac
modpack.broken = false;
modpack.bugged = false;
- //remove empty if the xml is bugged
- for(QString curr : modpack.oldVersions)
- {
- if(curr.isNull() || curr.isEmpty())
- {
+ // remove empty if the xml is bugged
+ for (QString curr : modpack.oldVersions) {
+ if (curr.isNull() || curr.isEmpty()) {
modpack.oldVersions.removeAll(curr);
modpack.bugged = true;
qWarning() << "Removed some empty versions from" << modpack.name;
}
}
- if(modpack.oldVersions.size() < 1)
- {
- if(!modpack.currentVersion.isNull() && !modpack.currentVersion.isEmpty())
- {
+ if (modpack.oldVersions.size() < 1) {
+ if (!modpack.currentVersion.isNull() && !modpack.currentVersion.isEmpty()) {
modpack.oldVersions.append(modpack.currentVersion);
qWarning() << "Added current version to oldVersions because oldVersions was empty! (" + modpack.name + ")";
- }
- else
- {
+ } else {
modpack.broken = true;
qWarning() << "Broken pack:" << modpack.name << " => No valid version!";
}
@@ -218,4 +198,4 @@ void PackFetchTask::fileDownloadAborted()
emit aborted();
}
-}
+} // namespace LegacyFTB
diff --git a/launcher/modplatform/legacy_ftb/PackFetchTask.h b/launcher/modplatform/legacy_ftb/PackFetchTask.h
index 8f3c4f3b..f2116ce9 100644
--- a/launcher/modplatform/legacy_ftb/PackFetchTask.h
+++ b/launcher/modplatform/legacy_ftb/PackFetchTask.h
@@ -1,41 +1,41 @@
#pragma once
-#include "net/NetJob.h"
-#include <QTemporaryDir>
#include <QByteArray>
#include <QObject>
+#include <QTemporaryDir>
+#include <memory>
#include "PackHelpers.h"
+#include "net/NetJob.h"
namespace LegacyFTB {
class PackFetchTask : public QObject {
-
Q_OBJECT
-public:
- PackFetchTask(shared_qobject_ptr<QNetworkAccessManager> network) : QObject(nullptr), m_network(network) {};
+ public:
+ PackFetchTask(shared_qobject_ptr<QNetworkAccessManager> network) : QObject(nullptr), m_network(network){};
virtual ~PackFetchTask() = default;
void fetch();
- void fetchPrivate(const QStringList &toFetch);
+ void fetchPrivate(const QStringList& toFetch);
-private:
+ private:
shared_qobject_ptr<QNetworkAccessManager> m_network;
NetJob::Ptr jobPtr;
- QByteArray publicModpacksXmlFileData;
- QByteArray thirdPartyModpacksXmlFileData;
+ std::shared_ptr<QByteArray> publicModpacksXmlFileData = std::make_shared<QByteArray>();
+ std::shared_ptr<QByteArray> thirdPartyModpacksXmlFileData = std::make_shared<QByteArray>();
- bool parseAndAddPacks(QByteArray &data, PackType packType, ModpackList &list);
+ bool parseAndAddPacks(QByteArray& data, PackType packType, ModpackList& list);
ModpackList publicPacks;
ModpackList thirdPartyPacks;
-protected slots:
+ protected slots:
void fileDownloadFinished();
void fileDownloadFailed(QString reason);
void fileDownloadAborted();
-signals:
+ signals:
void finished(ModpackList publicPacks, ModpackList thirdPartyPacks);
void failed(QString reason);
void aborted();
@@ -44,4 +44,4 @@ signals:
void privateFileDownloadFailed(QString reason, QString packCode);
};
-}
+} // namespace LegacyFTB
diff --git a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp
index 8d45fc5c..a4c78397 100644
--- a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp
+++ b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp
@@ -37,16 +37,16 @@
#include <QtConcurrent>
-#include "MMCZip.h"
#include "BaseInstance.h"
#include "FileSystem.h"
-#include "settings/INISettingsObject.h"
+#include "MMCZip.h"
+#include "minecraft/GradleSpecifier.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
-#include "minecraft/GradleSpecifier.h"
+#include "settings/INISettingsObject.h"
-#include "BuildConfig.h"
#include "Application.h"
+#include "BuildConfig.h"
namespace LegacyFTB {
@@ -65,6 +65,7 @@ void PackInstallTask::executeTask()
void PackInstallTask::downloadPack()
{
setStatus(tr("Downloading zip for %1").arg(m_pack.name));
+ setProgress(1, 4);
setAbortable(false);
archivePath = QString("%1/%2/%3").arg(m_pack.dir, m_version.replace(".", "_"), m_pack.file);
@@ -78,10 +79,10 @@ void PackInstallTask::downloadPack()
}
netJobContainer->addNetAction(Net::Download::makeFile(url, archivePath));
- connect(netJobContainer.get(), &NetJob::succeeded, this, &PackInstallTask::onDownloadSucceeded);
- connect(netJobContainer.get(), &NetJob::failed, this, &PackInstallTask::onDownloadFailed);
- connect(netJobContainer.get(), &NetJob::progress, this, &PackInstallTask::onDownloadProgress);
- connect(netJobContainer.get(), &NetJob::aborted, this, &PackInstallTask::onDownloadAborted);
+ connect(netJobContainer.get(), &NetJob::succeeded, this, &PackInstallTask::unzip);
+ connect(netJobContainer.get(), &NetJob::failed, this, &PackInstallTask::emitFailed);
+ connect(netJobContainer.get(), &NetJob::stepProgress, this, &PackInstallTask::propogateStepProgress);
+ connect(netJobContainer.get(), &NetJob::aborted, this, &PackInstallTask::emitAborted);
netJobContainer->start();
@@ -89,27 +90,6 @@ void PackInstallTask::downloadPack()
progress(1, 4);
}
-void PackInstallTask::onDownloadSucceeded()
-{
- unzip();
-}
-
-void PackInstallTask::onDownloadFailed(QString reason)
-{
- emitFailed(reason);
-}
-
-void PackInstallTask::onDownloadProgress(qint64 current, qint64 total)
-{
- progress(current, total * 4);
- setStatus(tr("Downloading zip for %1 (%2%)").arg(m_pack.name).arg(current / 10));
-}
-
-void PackInstallTask::onDownloadAborted()
-{
- emitAborted();
-}
-
void PackInstallTask::unzip()
{
setStatus(tr("Extracting modpack"));
@@ -119,16 +99,17 @@ void PackInstallTask::unzip()
QDir extractDir(m_stagingPath);
m_packZip.reset(new QuaZip(archivePath));
- if(!m_packZip->open(QuaZip::mdUnzip))
- {
+ if (!m_packZip->open(QuaZip::mdUnzip)) {
emitFailed(tr("Failed to open modpack file %1!").arg(archivePath));
return;
}
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
- m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), QOverload<QString, QString>::of(MMCZip::extractDir), archivePath, extractDir.absolutePath() + "/unzip");
+ m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), QOverload<QString, QString>::of(MMCZip::extractDir), archivePath,
+ extractDir.absolutePath() + "/unzip");
#else
- m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractDir, archivePath, extractDir.absolutePath() + "/unzip");
+ m_extractFuture =
+ QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractDir, archivePath, extractDir.absolutePath() + "/unzip");
#endif
connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, &PackInstallTask::onUnzipFinished);
connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::canceled, this, &PackInstallTask::onUnzipCanceled);
@@ -150,11 +131,9 @@ void PackInstallTask::install()
setStatus(tr("Installing modpack"));
progress(3, 4);
QDir unzipMcDir(m_stagingPath + "/unzip/minecraft");
- if(unzipMcDir.exists())
- {
- //ok, found minecraft dir, move contents to instance dir
- if(!QDir().rename(m_stagingPath + "/unzip/minecraft", m_stagingPath + "/.minecraft"))
- {
+ if (unzipMcDir.exists()) {
+ // ok, found minecraft dir, move contents to instance dir
+ if (!QDir().rename(m_stagingPath + "/unzip/minecraft", m_stagingPath + "/.minecraft")) {
emitFailed(tr("Failed to move unzipped Minecraft!"));
return;
}
@@ -171,23 +150,20 @@ void PackInstallTask::install()
bool fallback = true;
- //handle different versions
+ // handle different versions
QFile packJson(m_stagingPath + "/.minecraft/pack.json");
QDir jarmodDir = QDir(m_stagingPath + "/unzip/instMods");
- if(packJson.exists())
- {
+ if (packJson.exists()) {
packJson.open(QIODevice::ReadOnly | QIODevice::Text);
QJsonDocument doc = QJsonDocument::fromJson(packJson.readAll());
packJson.close();
- //we only care about the libs
+ // we only care about the libs
QJsonArray libs = doc.object().value("libraries").toArray();
- foreach (const QJsonValue &value, libs)
- {
+ foreach (const QJsonValue& value, libs) {
QString nameValue = value.toObject().value("name").toString();
- if(!nameValue.startsWith("net.minecraftforge"))
- {
+ if (!nameValue.startsWith("net.minecraftforge")) {
continue;
}
@@ -198,16 +174,13 @@ void PackInstallTask::install()
fallback = false;
break;
}
-
}
- if(jarmodDir.exists())
- {
+ if (jarmodDir.exists()) {
qDebug() << "Found jarmods, installing...";
QStringList jarmods;
- for (auto info: jarmodDir.entryInfoList(QDir::NoDotAndDotDot | QDir::Files))
- {
+ for (auto info : jarmodDir.entryInfoList(QDir::NoDotAndDotDot | QDir::Files)) {
qDebug() << "Jarmod:" << info.fileName();
jarmods.push_back(info.absoluteFilePath());
}
@@ -216,12 +189,11 @@ void PackInstallTask::install()
fallback = false;
}
- //just nuke unzip directory, it s not needed anymore
+ // just nuke unzip directory, it s not needed anymore
FS::deletePath(m_stagingPath + "/unzip");
- if(fallback)
- {
- //TODO: Some fallback mechanism... or just keep failing!
+ if (fallback) {
+ // TODO: Some fallback mechanism... or just keep failing!
emitFailed(tr("No installation method found!"));
return;
}
@@ -231,8 +203,7 @@ void PackInstallTask::install()
progress(4, 4);
instance.setName(name());
- if(m_instIcon == "default")
- {
+ if (m_instIcon == "default") {
m_instIcon = "ftb_logo";
}
instance.setIconKey(m_instIcon);
@@ -251,4 +222,4 @@ bool PackInstallTask::abort()
return InstanceTask::abort();
}
-}
+} // namespace LegacyFTB
diff --git a/launcher/modplatform/legacy_ftb/PackInstallTask.h b/launcher/modplatform/legacy_ftb/PackInstallTask.h
index da791e06..30ff4859 100644
--- a/launcher/modplatform/legacy_ftb/PackInstallTask.h
+++ b/launcher/modplatform/legacy_ftb/PackInstallTask.h
@@ -1,12 +1,12 @@
#pragma once
-#include "InstanceTask.h"
-#include "net/NetJob.h"
#include <quazip/quazip.h>
#include <quazip/quazipdir.h>
+#include "InstanceTask.h"
+#include "PackHelpers.h"
#include "meta/Index.h"
#include "meta/Version.h"
#include "meta/VersionList.h"
-#include "PackHelpers.h"
+#include "net/NetJob.h"
#include "net/NetJob.h"
@@ -14,36 +14,31 @@
namespace LegacyFTB {
-class PackInstallTask : public InstanceTask
-{
+class PackInstallTask : public InstanceTask {
Q_OBJECT
-public:
+ public:
explicit PackInstallTask(shared_qobject_ptr<QNetworkAccessManager> network, Modpack pack, QString version);
- virtual ~PackInstallTask(){}
+ virtual ~PackInstallTask() {}
bool canAbort() const override { return true; }
bool abort() override;
-protected:
+ protected:
//! Entry point for tasks.
virtual void executeTask() override;
-private:
+ private:
void downloadPack();
void unzip();
void install();
-private slots:
- void onDownloadSucceeded();
- void onDownloadFailed(QString reason);
- void onDownloadProgress(qint64 current, qint64 total);
- void onDownloadAborted();
+ private slots:
void onUnzipFinished();
void onUnzipCanceled();
-private: /* data */
+ private: /* data */
shared_qobject_ptr<QNetworkAccessManager> m_network;
bool abortable = false;
std::unique_ptr<QuaZip> m_packZip;
@@ -56,4 +51,4 @@ private: /* data */
QString m_version;
};
-}
+} // namespace LegacyFTB
diff --git a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp
deleted file mode 100644
index 68d4751c..00000000
--- a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp
+++ /dev/null
@@ -1,387 +0,0 @@
-// SPDX-License-Identifier: GPL-3.0-only
-/*
- * Prism Launcher - Minecraft Launcher
- * Copyright (C) 2022 flowln <flowlnlnln@gmail.com>
- * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
- * 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 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org>
- * Copyright 2020-2021 Petr Mrazek <peterix@gmail.com>
- *
- * 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 "FTBPackInstallTask.h"
-
-#include "FileSystem.h"
-#include "Json.h"
-#include "minecraft/MinecraftInstance.h"
-#include "minecraft/PackProfile.h"
-#include "modplatform/flame/PackManifest.h"
-#include "net/ChecksumValidator.h"
-#include "settings/INISettingsObject.h"
-
-#include "Application.h"
-#include "BuildConfig.h"
-#include "ui/dialogs/BlockedModsDialog.h"
-
-namespace ModpacksCH {
-
-PackInstallTask::PackInstallTask(Modpack pack, QString version, QWidget* parent)
- : m_pack(std::move(pack)), m_version_name(std::move(version)), m_parent(parent)
-{}
-
-bool PackInstallTask::abort()
-{
- if (!canAbort())
- return false;
-
- bool aborted = true;
-
- if (m_net_job)
- aborted &= m_net_job->abort();
- if (m_mod_id_resolver_task)
- aborted &= m_mod_id_resolver_task->abort();
-
- return aborted ? InstanceTask::abort() : false;
-}
-
-void PackInstallTask::executeTask()
-{
- setStatus(tr("Getting the manifest..."));
- setAbortable(false);
-
- // Find pack version
- auto version_it = std::find_if(m_pack.versions.constBegin(), m_pack.versions.constEnd(),
- [this](ModpacksCH::VersionInfo const& a) { return a.name == m_version_name; });
-
- if (version_it == m_pack.versions.constEnd()) {
- emitFailed(tr("Failed to find pack version %1").arg(m_version_name));
- return;
- }
-
- auto version = *version_it;
-
- 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.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;
-
- setAbortable(true);
- netJob->start();
-}
-
-void PackInstallTask::onManifestDownloadSucceeded()
-{
- m_net_job.reset();
-
- QJsonParseError parse_error{};
- QJsonDocument doc = QJsonDocument::fromJson(m_response, &parse_error);
- if (parse_error.error != QJsonParseError::NoError) {
- qWarning() << "Error while parsing JSON response from ModpacksCH at " << parse_error.offset
- << " reason: " << parse_error.errorString();
- qWarning() << m_response;
- return;
- }
-
- ModpacksCH::Version version;
- try {
- auto obj = Json::requireObject(doc);
- ModpacksCH::loadVersion(version, obj);
- } catch (const JSONValidationError& e) {
- emitFailed(tr("Could not understand pack manifest:\n") + e.cause());
- return;
- }
-
- m_version = version;
-
- resolveMods();
-}
-
-void PackInstallTask::resolveMods()
-{
- setStatus(tr("Resolving mods..."));
- setAbortable(false);
- setProgress(0, 100);
-
- m_file_id_map.clear();
-
- Flame::Manifest manifest;
- int index = 0;
-
- for (auto const& file : m_version.files) {
- if (!file.serverOnly && file.url.isEmpty()) {
- if (file.curseforge.file_id <= 0) {
- emitFailed(tr("Invalid manifest: There's no information available to download the file '%1'!").arg(file.name));
- return;
- }
-
- Flame::File flame_file;
- flame_file.projectId = file.curseforge.project_id;
- flame_file.fileId = file.curseforge.file_id;
- flame_file.hash = file.sha1;
-
- manifest.files.insert(flame_file.fileId, flame_file);
- m_file_id_map.append(flame_file.fileId);
- } else {
- m_file_id_map.append(-1);
- }
-
- index++;
- }
-
- 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);
- connect(m_mod_id_resolver_task.get(), &Flame::FileResolvingTask::aborted, this, &PackInstallTask::abort);
- connect(m_mod_id_resolver_task.get(), &Flame::FileResolvingTask::progress, this, &PackInstallTask::setProgress);
-
- setAbortable(true);
-
- m_mod_id_resolver_task->start();
-}
-
-void PackInstallTask::onResolveModsSucceeded()
-{
- auto anyBlocked = false;
-
- Flame::Manifest results = m_mod_id_resolver_task->getResults();
- for (int index = 0; index < m_file_id_map.size(); index++) {
- auto const file_id = m_file_id_map.at(index);
- if (file_id < 0)
- continue;
-
- Flame::File results_file = results.files[file_id];
- VersionFile& local_file = m_version.files[index];
-
- // First check for blocked mods
- if (!results_file.resolved || results_file.url.isEmpty()) {
- BlockedMod blocked_mod;
- blocked_mod.name = local_file.name;
- blocked_mod.websiteUrl = results_file.websiteUrl;
- blocked_mod.hash = results_file.hash;
- blocked_mod.matched = false;
- blocked_mod.localPath = "";
- blocked_mod.targetFolder = results_file.targetFolder;
-
- m_blocked_mods.append(blocked_mod);
-
- anyBlocked = true;
- } else {
- local_file.url = results_file.url.toString();
- }
- }
-
- m_mod_id_resolver_task.reset();
-
- if (anyBlocked) {
- qDebug() << "Blocked files found, displaying file list";
-
- BlockedModsDialog message_dialog(m_parent, tr("Blocked files found"),
- tr("The following files are not available for download in third party launchers.<br/>"
- "You will need to manually download them and add them to the instance."),
- m_blocked_mods);
-
- message_dialog.setModal(true);
-
- if (message_dialog.exec() == QDialog::Accepted) {
- qDebug() << "Post dialog blocked mods list: " << m_blocked_mods;
- createInstance();
- } else {
- abort();
- }
-
- } else {
- createInstance();
- }
-}
-
-void PackInstallTask::createInstance()
-{
- setAbortable(false);
-
- setStatus(tr("Creating the instance..."));
- QCoreApplication::processEvents();
-
- auto instanceConfigPath = FS::PathCombine(m_stagingPath, "instance.cfg");
- auto instanceSettings = std::make_shared<INISettingsObject>(instanceConfigPath);
-
- MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath);
- auto components = instance.getPackProfile();
- components->buildingFromScratch();
-
- for (auto target : m_version.targets) {
- if (target.type == "game" && target.name == "minecraft") {
- components->setComponentVersion("net.minecraft", target.version, true);
- break;
- }
- }
-
- for (auto target : m_version.targets) {
- if (target.type != "modloader")
- continue;
-
- if (target.name == "forge") {
- components->setComponentVersion("net.minecraftforge", target.version);
- } else if (target.name == "fabric") {
- components->setComponentVersion("net.fabricmc.fabric-loader", target.version);
- }
- }
-
- // install any jar mods
- QDir jarModsDir(FS::PathCombine(m_stagingPath, "minecraft", "jarmods"));
- if (jarModsDir.exists()) {
- QStringList jarMods;
-
- for (const auto& info : jarModsDir.entryInfoList(QDir::NoDotAndDotDot | QDir::Files)) {
- jarMods.push_back(info.absoluteFilePath());
- }
-
- components->installJarMods(jarMods);
- }
-
- components->saveNow();
-
- instance.setName(name());
- instance.setIconKey(m_instIcon);
- instance.setManagedPack("modpacksch", QString::number(m_pack.id), m_pack.name, QString::number(m_version.id), m_version.name);
-
- instance.saveNow();
-
- onCreateInstanceSucceeded();
-}
-
-void PackInstallTask::onCreateInstanceSucceeded()
-{
- downloadPack();
-}
-
-void PackInstallTask::downloadPack()
-{
- setStatus(tr("Downloading mods..."));
- setAbortable(false);
-
- auto jobPtr = makeShared<NetJob>(tr("Mod download"), APPLICATION->network());
- for (auto const& file : m_version.files) {
- if (file.serverOnly || file.url.isEmpty())
- continue;
-
- auto path = FS::PathCombine(m_stagingPath, ".minecraft", file.path, file.name);
- qDebug() << "Will try to download" << file.url << "to" << path;
-
- QFileInfo file_info(file.name);
-
- auto dl = Net::Download::makeFile(file.url, path);
- if (!file.sha1.isEmpty()) {
- auto rawSha1 = QByteArray::fromHex(file.sha1.toLatin1());
- dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawSha1));
- }
-
- jobPtr->addNetAction(dl);
- }
-
- 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;
-
- setAbortable(true);
- jobPtr->start();
-}
-
-void PackInstallTask::onModDownloadSucceeded()
-{
- m_net_job.reset();
- if (!m_blocked_mods.isEmpty()) {
- copyBlockedMods();
- }
- emitSucceeded();
-}
-
-void PackInstallTask::onManifestDownloadFailed(QString reason)
-{
- m_net_job.reset();
- emitFailed(reason);
-}
-void PackInstallTask::onResolveModsFailed(QString reason)
-{
- m_net_job.reset();
- emitFailed(reason);
-}
-void PackInstallTask::onCreateInstanceFailed(QString reason)
-{
- emitFailed(reason);
-}
-void PackInstallTask::onModDownloadFailed(QString reason)
-{
- m_net_job.reset();
- emitFailed(reason);
-}
-
-/// @brief copy the matched blocked mods to the instance staging area
-void PackInstallTask::copyBlockedMods()
-{
- setStatus(tr("Copying Blocked Mods..."));
- setAbortable(false);
- int i = 0;
- int total = m_blocked_mods.length();
- setProgress(i, total);
- for (auto const& mod : m_blocked_mods) {
- if (!mod.matched) {
- qDebug() << mod.name << "was not matched to a local file, skipping copy";
- continue;
- }
-
- auto dest_path = FS::PathCombine(m_stagingPath, ".minecraft", mod.targetFolder, mod.name);
-
- setStatus(tr("Copying Blocked Mods (%1 out of %2 are done)").arg(QString::number(i), QString::number(total)));
-
- qDebug() << "Will try to copy" << mod.localPath << "to" << dest_path;
-
- if (!FS::copy(mod.localPath, dest_path)()) {
- qDebug() << "Copy of" << mod.localPath << "to" << dest_path << "Failed";
- }
-
- i++;
- setProgress(i, total);
- }
-
- setAbortable(true);
-}
-
-} // namespace ModpacksCH
diff --git a/launcher/modplatform/modpacksch/FTBPackInstallTask.h b/launcher/modplatform/modpacksch/FTBPackInstallTask.h
deleted file mode 100644
index 97b1eb0b..00000000
--- a/launcher/modplatform/modpacksch/FTBPackInstallTask.h
+++ /dev/null
@@ -1,101 +0,0 @@
-// SPDX-License-Identifier: GPL-3.0-only
-/*
- * Prism Launcher - 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/>.
- *
- * This file incorporates work covered by the following copyright and
- * permission notice:
- *
- * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org>
- * Copyright 2020-2021 Petr Mrazek <peterix@gmail.com>
- *
- * 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 "FTBPackManifest.h"
-
-#include "InstanceTask.h"
-#include "QObjectPtr.h"
-#include "modplatform/flame/FileResolvingTask.h"
-#include "net/NetJob.h"
-#include "ui/dialogs/BlockedModsDialog.h"
-
-#include <QWidget>
-
-namespace ModpacksCH {
-
-class PackInstallTask final : public InstanceTask
-{
- Q_OBJECT
-
-public:
- explicit PackInstallTask(Modpack pack, QString version, QWidget* parent = nullptr);
- ~PackInstallTask() override = default;
-
- bool abort() override;
-
-protected:
- void executeTask() override;
-
-private slots:
- void onManifestDownloadSucceeded();
- void onResolveModsSucceeded();
- void onCreateInstanceSucceeded();
- void onModDownloadSucceeded();
-
- void onManifestDownloadFailed(QString reason);
- void onResolveModsFailed(QString reason);
- void onCreateInstanceFailed(QString reason);
- void onModDownloadFailed(QString reason);
-
-private:
- void resolveMods();
- void createInstance();
- void downloadPack();
- void copyBlockedMods();
-
-private:
- NetJob::Ptr m_net_job = nullptr;
- shared_qobject_ptr<Flame::FileResolvingTask> m_mod_id_resolver_task = nullptr;
-
- QList<int> m_file_id_map;
-
- QByteArray m_response;
-
- Modpack m_pack;
- QString m_version_name;
- Version m_version;
-
- QMap<QString, QString> m_files_to_copy;
- QList<BlockedMod> m_blocked_mods;
-
- //FIXME: nuke
- QWidget* m_parent;
-};
-
-}
diff --git a/launcher/modplatform/modpacksch/FTBPackManifest.cpp b/launcher/modplatform/modpacksch/FTBPackManifest.cpp
deleted file mode 100644
index 421527ae..00000000
--- a/launcher/modplatform/modpacksch/FTBPackManifest.cpp
+++ /dev/null
@@ -1,195 +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 2020 Jamie Mansfield <jmansfield@cadixdev.org>
- * Copyright 2020-2021 Petr Mrazek <peterix@gmail.com>
- *
- * 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 "FTBPackManifest.h"
-
-#include "Json.h"
-
-static void loadSpecs(ModpacksCH::Specs & s, QJsonObject & obj)
-{
- s.id = Json::requireInteger(obj, "id");
- s.minimum = Json::requireInteger(obj, "minimum");
- s.recommended = Json::requireInteger(obj, "recommended");
-}
-
-static void loadTag(ModpacksCH::Tag & t, QJsonObject & obj)
-{
- t.id = Json::requireInteger(obj, "id");
- t.name = Json::requireString(obj, "name");
-}
-
-static void loadArt(ModpacksCH::Art & a, QJsonObject & obj)
-{
- a.id = Json::requireInteger(obj, "id");
- a.url = Json::requireString(obj, "url");
- a.type = Json::requireString(obj, "type");
- a.width = Json::requireInteger(obj, "width");
- a.height = Json::requireInteger(obj, "height");
- a.compressed = Json::requireBoolean(obj, "compressed");
- a.sha1 = Json::requireString(obj, "sha1");
- a.size = Json::requireInteger(obj, "size");
- a.updated = Json::requireInteger(obj, "updated");
-}
-
-static void loadAuthor(ModpacksCH::Author & a, QJsonObject & obj)
-{
- a.id = Json::requireInteger(obj, "id");
- a.name = Json::requireString(obj, "name");
- a.type = Json::requireString(obj, "type");
- a.website = Json::requireString(obj, "website");
- a.updated = Json::requireInteger(obj, "updated");
-}
-
-static void loadVersionInfo(ModpacksCH::VersionInfo & v, QJsonObject & obj)
-{
- v.id = Json::requireInteger(obj, "id");
- v.name = Json::requireString(obj, "name");
- v.type = Json::requireString(obj, "type");
- v.updated = Json::requireInteger(obj, "updated");
- auto specs = Json::requireObject(obj, "specs");
- loadSpecs(v.specs, specs);
-}
-
-void ModpacksCH::loadModpack(ModpacksCH::Modpack & m, QJsonObject & obj)
-{
- m.id = Json::requireInteger(obj, "id");
- m.name = Json::requireString(obj, "name");
- m.synopsis = Json::requireString(obj, "synopsis");
- m.description = Json::requireString(obj, "description");
- m.type = Json::requireString(obj, "type");
- m.featured = Json::requireBoolean(obj, "featured");
- m.installs = Json::requireInteger(obj, "installs");
- m.plays = Json::requireInteger(obj, "plays");
- m.updated = Json::requireInteger(obj, "updated");
- m.refreshed = Json::requireInteger(obj, "refreshed");
- auto artArr = Json::requireArray(obj, "art");
- for (QJsonValueRef artRaw : artArr)
- {
- auto artObj = Json::requireObject(artRaw);
- ModpacksCH::Art art;
- loadArt(art, artObj);
- m.art.append(art);
- }
- auto authorArr = Json::requireArray(obj, "authors");
- for (QJsonValueRef authorRaw : authorArr)
- {
- auto authorObj = Json::requireObject(authorRaw);
- ModpacksCH::Author author;
- loadAuthor(author, authorObj);
- m.authors.append(author);
- }
- auto versionArr = Json::requireArray(obj, "versions");
- for (QJsonValueRef versionRaw : versionArr)
- {
- auto versionObj = Json::requireObject(versionRaw);
- ModpacksCH::VersionInfo version;
- loadVersionInfo(version, versionObj);
- m.versions.append(version);
- }
- auto tagArr = Json::requireArray(obj, "tags");
- for (QJsonValueRef tagRaw : tagArr)
- {
- auto tagObj = Json::requireObject(tagRaw);
- ModpacksCH::Tag tag;
- loadTag(tag, tagObj);
- m.tags.append(tag);
- }
- m.updated = Json::requireInteger(obj, "updated");
-}
-
-static void loadVersionTarget(ModpacksCH::VersionTarget & a, QJsonObject & obj)
-{
- a.id = Json::requireInteger(obj, "id");
- a.name = Json::requireString(obj, "name");
- a.type = Json::requireString(obj, "type");
- a.version = Json::requireString(obj, "version");
- a.updated = Json::requireInteger(obj, "updated");
-}
-
-static void loadVersionFile(ModpacksCH::VersionFile & a, QJsonObject & obj)
-{
- a.id = Json::requireInteger(obj, "id");
- a.type = Json::requireString(obj, "type");
- a.path = Json::requireString(obj, "path");
- a.name = Json::requireString(obj, "name");
- a.version = Json::requireString(obj, "version");
- a.url = Json::ensureString(obj, "url"); // optional
- a.sha1 = Json::requireString(obj, "sha1");
- a.size = Json::requireInteger(obj, "size");
- a.clientOnly = Json::requireBoolean(obj, "clientonly");
- a.serverOnly = Json::requireBoolean(obj, "serveronly");
- a.optional = Json::requireBoolean(obj, "optional");
- a.updated = Json::requireInteger(obj, "updated");
- auto curseforgeObj = Json::ensureObject(obj, "curseforge"); // optional
- a.curseforge.project_id = Json::ensureInteger(curseforgeObj, "project");
- a.curseforge.file_id = Json::ensureInteger(curseforgeObj, "file");
-}
-
-void ModpacksCH::loadVersion(ModpacksCH::Version & m, QJsonObject & obj)
-{
- m.id = Json::requireInteger(obj, "id");
- m.parent = Json::requireInteger(obj, "parent");
- m.name = Json::requireString(obj, "name");
- m.type = Json::requireString(obj, "type");
- m.installs = Json::requireInteger(obj, "installs");
- m.plays = Json::requireInteger(obj, "plays");
- m.updated = Json::requireInteger(obj, "updated");
- m.refreshed = Json::requireInteger(obj, "refreshed");
- auto specs = Json::requireObject(obj, "specs");
- loadSpecs(m.specs, specs);
- auto targetArr = Json::requireArray(obj, "targets");
- for (QJsonValueRef targetRaw : targetArr)
- {
- auto versionObj = Json::requireObject(targetRaw);
- ModpacksCH::VersionTarget target;
- loadVersionTarget(target, versionObj);
- m.targets.append(target);
- }
- auto fileArr = Json::requireArray(obj, "files");
- for (QJsonValueRef fileRaw : fileArr)
- {
- auto fileObj = Json::requireObject(fileRaw);
- ModpacksCH::VersionFile file;
- loadVersionFile(file, fileObj);
- m.files.append(file);
- }
-}
-
-//static void loadVersionChangelog(ModpacksCH::VersionChangelog & m, QJsonObject & obj)
-//{
-// m.content = Json::requireString(obj, "content");
-// m.updated = Json::requireInteger(obj, "updated");
-//}
diff --git a/launcher/modplatform/modpacksch/FTBPackManifest.h b/launcher/modplatform/modpacksch/FTBPackManifest.h
deleted file mode 100644
index a8b6f35e..00000000
--- a/launcher/modplatform/modpacksch/FTBPackManifest.h
+++ /dev/null
@@ -1,168 +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 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org>
- * Copyright 2020 Petr Mrazek <peterix@gmail.com>
- *
- * 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 <QVector>
-#include <QUrl>
-#include <QJsonObject>
-#include <QMetaType>
-
-namespace ModpacksCH
-{
-
-struct Specs
-{
- int id;
- int minimum;
- int recommended;
-};
-
-struct Tag
-{
- int id;
- QString name;
-};
-
-struct Art
-{
- int id;
- QString url;
- QString type;
- int width;
- int height;
- bool compressed;
- QString sha1;
- int size;
- int64_t updated;
-};
-
-struct Author
-{
- int id;
- QString name;
- QString type;
- QString website;
- int64_t updated;
-};
-
-struct VersionInfo
-{
- int id;
- QString name;
- QString type;
- int64_t updated;
- Specs specs;
-};
-
-struct Modpack
-{
- int id;
- QString name;
- QString synopsis;
- QString description;
- QString type;
- bool featured;
- int installs;
- int plays;
- int64_t updated;
- int64_t refreshed;
- QVector<Art> art;
- QVector<Author> authors;
- QVector<VersionInfo> versions;
- QVector<Tag> tags;
-};
-
-struct VersionTarget
-{
- int id;
- QString type;
- QString name;
- QString version;
- int64_t updated;
-};
-
-struct VersionFileCurseForge
-{
- int project_id;
- int file_id;
-};
-
-struct VersionFile
-{
- int id;
- QString type;
- QString path;
- QString name;
- QString version;
- QString url;
- QString sha1;
- int size;
- bool clientOnly;
- bool serverOnly;
- bool optional;
- int64_t updated;
- VersionFileCurseForge curseforge;
-};
-
-struct Version
-{
- int id;
- int parent;
- QString name;
- QString type;
- int installs;
- int plays;
- int64_t updated;
- int64_t refreshed;
- Specs specs;
- QVector<VersionTarget> targets;
- QVector<VersionFile> files;
-};
-
-struct VersionChangelog
-{
- QString content;
- int64_t updated;
-};
-
-void loadModpack(Modpack & m, QJsonObject & obj);
-
-void loadVersion(Version & m, QJsonObject & obj);
-}
-
-Q_DECLARE_METATYPE(ModpacksCH::Modpack)
diff --git a/launcher/modplatform/modrinth/ModrinthAPI.cpp b/launcher/modplatform/modrinth/ModrinthAPI.cpp
index 29e3d129..364cf3f3 100644
--- a/launcher/modplatform/modrinth/ModrinthAPI.cpp
+++ b/launcher/modplatform/modrinth/ModrinthAPI.cpp
@@ -9,19 +9,17 @@
#include "net/NetJob.h"
#include "net/Upload.h"
-Task::Ptr ModrinthAPI::currentVersion(QString hash, QString hash_format, QByteArray* response)
+Task::Ptr ModrinthAPI::currentVersion(QString hash, QString hash_format, std::shared_ptr<QByteArray> response)
{
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.get(), &NetJob::finished, [response] { delete response; });
-
return netJob;
}
-Task::Ptr ModrinthAPI::currentVersions(const QStringList& hashes, QString hash_format, QByteArray* response)
+Task::Ptr ModrinthAPI::currentVersions(const QStringList& hashes, QString hash_format, std::shared_ptr<QByteArray> response)
{
auto netJob = makeShared<NetJob>(QString("Modrinth::GetCurrentVersions"), APPLICATION->network());
@@ -35,8 +33,6 @@ Task::Ptr ModrinthAPI::currentVersions(const QStringList& hashes, QString hash_f
netJob->addNetAction(Net::Upload::makeByteArray(QString(BuildConfig.MODRINTH_PROD_URL + "/version_files"), response, body_raw));
- QObject::connect(netJob.get(), &NetJob::finished, [response] { delete response; });
-
return netJob;
}
@@ -44,7 +40,7 @@ Task::Ptr ModrinthAPI::latestVersion(QString hash,
QString hash_format,
std::optional<std::list<Version>> mcVersions,
std::optional<ModLoaderTypes> loaders,
- QByteArray* response)
+ std::shared_ptr<QByteArray> response)
{
auto netJob = makeShared<NetJob>(QString("Modrinth::GetLatestVersion"), APPLICATION->network());
@@ -67,8 +63,6 @@ Task::Ptr 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.get(), &NetJob::finished, [response] { delete response; });
-
return netJob;
}
@@ -76,7 +70,7 @@ Task::Ptr ModrinthAPI::latestVersions(const QStringList& hashes,
QString hash_format,
std::optional<std::list<Version>> mcVersions,
std::optional<ModLoaderTypes> loaders,
- QByteArray* response)
+ std::shared_ptr<QByteArray> response)
{
auto netJob = makeShared<NetJob>(QString("Modrinth::GetLatestVersions"), APPLICATION->network());
@@ -101,22 +95,16 @@ Task::Ptr ModrinthAPI::latestVersions(const QStringList& hashes,
netJob->addNetAction(Net::Upload::makeByteArray(QString(BuildConfig.MODRINTH_PROD_URL + "/version_files/update"), response, body_raw));
- QObject::connect(netJob.get(), &NetJob::finished, [response] { delete response; });
-
return netJob;
}
-Task::Ptr ModrinthAPI::getProjects(QStringList addonIds, QByteArray* response) const
+Task::Ptr ModrinthAPI::getProjects(QStringList addonIds, std::shared_ptr<QByteArray> response) const
{
auto netJob = makeShared<NetJob>(QString("Modrinth::GetProjects"), APPLICATION->network());
auto searchUrl = getMultipleModInfoURL(addonIds);
netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), response));
- QObject::connect(netJob.get(), &NetJob::finished, [response, netJob] {
- delete response;
- });
-
return netJob;
}
diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h
index b91ac5c1..58af14cc 100644
--- a/launcher/modplatform/modrinth/ModrinthAPI.h
+++ b/launcher/modplatform/modrinth/ModrinthAPI.h
@@ -12,27 +12,23 @@
class ModrinthAPI : public NetworkResourceAPI {
public:
- auto currentVersion(QString hash,
- QString hash_format,
- QByteArray* response) -> Task::Ptr;
+ auto currentVersion(QString hash, QString hash_format, std::shared_ptr<QByteArray> response) -> Task::Ptr;
- auto currentVersions(const QStringList& hashes,
- QString hash_format,
- QByteArray* response) -> Task::Ptr;
+ auto currentVersions(const QStringList& hashes, QString hash_format, std::shared_ptr<QByteArray> response) -> Task::Ptr;
auto latestVersion(QString hash,
QString hash_format,
std::optional<std::list<Version>> mcVersions,
std::optional<ModLoaderTypes> loaders,
- QByteArray* response) -> Task::Ptr;
+ std::shared_ptr<QByteArray> response) -> Task::Ptr;
auto latestVersions(const QStringList& hashes,
QString hash_format,
- std::optional<std::list<Version>> mcVersions,
- std::optional<ModLoaderTypes> loaders,
- QByteArray* response) -> Task::Ptr;
+ std::optional<std::list<Version>> mcVersions,
+ std::optional<ModLoaderTypes> loaders,
+ std::shared_ptr<QByteArray> response) -> Task::Ptr;
- Task::Ptr getProjects(QStringList addonIds, QByteArray* response) const override;
+ Task::Ptr getProjects(QStringList addonIds, std::shared_ptr<QByteArray> response) const override;
public:
[[nodiscard]] auto getSortingMethods() const -> QList<ResourceAPI::SortingMethod> override;
@@ -42,7 +38,7 @@ class ModrinthAPI : public NetworkResourceAPI {
static auto getModLoaderStrings(const ModLoaderTypes types) -> const QStringList
{
QStringList l;
- for (auto loader : {Forge, Fabric, Quilt}) {
+ for (auto loader : { Forge, Fabric, Quilt, LiteLoader }) {
if (types & loader) {
l << getModLoaderString(loader);
}
@@ -55,8 +51,7 @@ class ModrinthAPI : public NetworkResourceAPI {
static auto getModLoaderFilters(ModLoaderTypes types) -> const QString
{
QStringList l;
- for (auto loader : getModLoaderStrings(types))
- {
+ for (auto loader : getModLoaderStrings(types)) {
l << QString("\"categories:%1\"").arg(loader);
}
return l.join(',');
@@ -97,7 +92,7 @@ class ModrinthAPI : public NetworkResourceAPI {
{
if (args.loaders.has_value()) {
if (!validateModLoaders(args.loaders.value())) {
- qWarning() << "Modrinth only have Forge and Fabric-compatible mods!";
+ qWarning() << "Modrinth - or our interface - does not support any the provided mod loaders!";
return {};
}
}
@@ -139,16 +134,22 @@ class ModrinthAPI : public NetworkResourceAPI {
auto getGameVersionsArray(std::list<Version> mcVersions) const -> QString
{
QString s;
- for(auto& ver : mcVersions){
+ for (auto& ver : mcVersions) {
s += QString("\"versions:%1\",").arg(ver.toString());
}
- s.remove(s.length() - 1, 1); //remove last comma
+ s.remove(s.length() - 1, 1); // remove last comma
return s.isEmpty() ? QString() : s;
}
- inline auto validateModLoaders(ModLoaderTypes loaders) const -> bool
- {
- return loaders & (Forge | Fabric | Quilt);
- }
+ static inline auto validateModLoaders(ModLoaderTypes loaders) -> bool { return loaders & (Forge | Fabric | Quilt | LiteLoader); }
+ [[nodiscard]] std::optional<QString> getDependencyURL(DependencySearchArgs const& args) const override
+ {
+ return args.dependency.version.length() != 0 ? QString("%1/version/%2").arg(BuildConfig.MODRINTH_PROD_URL, args.dependency.version)
+ : QString("%1/project/%2/version?game_versions=[\"%3\"]&loaders=[\"%4\"]")
+ .arg(BuildConfig.MODRINTH_PROD_URL)
+ .arg(args.dependency.addonId.toString())
+ .arg(args.mcVersion.toString())
+ .arg(getModLoaderStrings(args.loader).join("\",\""));
+ };
};
diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp
index d1be7209..a7c22832 100644
--- a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp
+++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp
@@ -53,12 +53,11 @@ void ModrinthCheckUpdate::executeTask()
// (though it will rarely happen, if at all)
if (mod->metadata()->hash_format != best_hash_type) {
auto hash_task = Hashing::createModrinthHasher(mod->fileinfo().absoluteFilePath());
- connect(hash_task.get(), &Task::succeeded, [&] {
- QString hash (hash_task->getResult());
+ connect(hash_task.get(), &Hashing::Hasher::resultsReady, [&hashes, &mappings, mod](QString hash) {
hashes.append(hash);
mappings.insert(hash, mod);
});
- connect(hash_task.get(), &Task::failed, [this, hash_task] { failed("Failed to generate hash"); });
+ connect(hash_task.get(), &Task::failed, [this] { failed("Failed to generate hash"); });
hashing_task.addTask(hash_task);
} else {
hashes.append(hash);
@@ -67,11 +66,11 @@ void ModrinthCheckUpdate::executeTask()
}
QEventLoop loop;
- connect(&hashing_task, &Task::finished, [&loop]{ loop.quit(); });
+ connect(&hashing_task, &Task::finished, [&loop] { loop.quit(); });
hashing_task.start();
loop.exec();
- auto* response = new QByteArray();
+ auto response = std::make_shared<QByteArray>();
auto job = api.latestVersions(hashes, best_hash_type, m_game_versions, m_loaders, response);
QEventLoop lock;
@@ -112,7 +111,8 @@ void ModrinthCheckUpdate::executeTask()
// so we may want to filter it
QString loader_filter;
if (m_loaders.has_value()) {
- static auto flags = { ResourceAPI::ModLoaderType::Forge, ResourceAPI::ModLoaderType::Fabric, ResourceAPI::ModLoaderType::Quilt };
+ 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);
@@ -122,7 +122,8 @@ void ModrinthCheckUpdate::executeTask()
}
// Currently, we rely on a couple heuristics to determine whether an update is actually available or not:
- // - The file needs to be preferred: It is either the primary file, or the one found via (explicit) usage of the loader_filter
+ // - The file needs to be preferred: It is either the primary file, or the one found via (explicit) usage of the
+ // loader_filter
// - The version reported by the JAR is different from the version reported by the indexed version (it's usually the case)
// Such is the pain of having arbitrary files for a given version .-.
@@ -149,19 +150,19 @@ void ModrinthCheckUpdate::executeTask()
continue;
// Fake pack with the necessary info to pass to the download task :)
- ModPlatform::IndexedPack pack;
- pack.name = mod->name();
- pack.slug = mod->metadata()->slug;
- pack.addonId = mod->metadata()->project_id;
- pack.websiteUrl = mod->homeurl();
+ auto pack = std::make_shared<ModPlatform::IndexedPack>();
+ pack->name = mod->name();
+ pack->slug = mod->metadata()->slug;
+ pack->addonId = mod->metadata()->project_id;
+ pack->websiteUrl = mod->homeurl();
for (auto& author : mod->authors())
- pack.authors.append({ author });
- pack.description = mod->description();
- pack.provider = ModPlatform::ResourceProvider::MODRINTH;
+ pack->authors.append({ author });
+ pack->description = mod->description();
+ pack->provider = ModPlatform::ResourceProvider::MODRINTH;
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,
+ m_updatable.emplace_back(pack->name, hash, mod->version(), project_ver.version_number, project_ver.changelog,
ModPlatform::ResourceProvider::MODRINTH, download_task);
}
}
diff --git a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp
index 6814e645..76f07277 100644
--- a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp
+++ b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp
@@ -11,6 +11,7 @@
#include "net/ChecksumValidator.h"
+#include "net/NetJob.h"
#include "settings/INISettingsObject.h"
#include "ui/dialogs/CustomMessageBox.h"
@@ -213,7 +214,7 @@ bool ModrinthCreationTask::createInstance()
if (m_instIcon != "default") {
instance.setIconKey(m_instIcon);
- } else {
+ } else if (!m_managed_id.isEmpty()) {
instance.setIconKey("modrinth");
}
@@ -223,7 +224,7 @@ bool ModrinthCreationTask::createInstance()
instance.setName(name());
instance.saveNow();
- m_files_job.reset(new NetJob(tr("Mod download"), APPLICATION->network()));
+ m_files_job.reset(new NetJob(tr("Mod Download Modrinth"), APPLICATION->network()));
auto root_modpack_path = FS::PathCombine(m_stagingPath, ".minecraft");
auto root_modpack_url = QUrl::fromLocalFile(root_modpack_path);
@@ -262,7 +263,11 @@ bool ModrinthCreationTask::createInstance()
setError(reason);
});
connect(m_files_job.get(), &NetJob::finished, &loop, &QEventLoop::quit);
- connect(m_files_job.get(), &NetJob::progress, [&](qint64 current, qint64 total) { setProgress(current, total); });
+ connect(m_files_job.get(), &NetJob::progress, [&](qint64 current, qint64 total) {
+ setDetails(tr("%1 out of %2 complete").arg(current).arg(total));
+ setProgress(current, total);
+ });
+ connect(m_files_job.get(), &NetJob::stepProgress, this, &ModrinthCreationTask::propogateStepProgress);
setStatus(tr("Downloading mods..."));
m_files_job->start();
diff --git a/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp b/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp
new file mode 100644
index 00000000..30fe566d
--- /dev/null
+++ b/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp
@@ -0,0 +1,332 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2023 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 "ModrinthPackExportTask.h"
+
+#include <QCryptographicHash>
+#include <QFileInfo>
+#include <QMessageBox>
+#include <QtConcurrentRun>
+#include "Json.h"
+#include "MMCZip.h"
+#include "minecraft/PackProfile.h"
+#include "minecraft/mod/ModFolderModel.h"
+
+const QStringList ModrinthPackExportTask::PREFIXES({ "mods/", "coremods/", "resourcepacks/", "texturepacks/", "shaderpacks/" });
+const QStringList ModrinthPackExportTask::FILE_EXTENSIONS({ "jar", "litemod", "zip" });
+
+ModrinthPackExportTask::ModrinthPackExportTask(const QString& name,
+ const QString& version,
+ const QString& summary,
+ InstancePtr instance,
+ const QString& output,
+ MMCZip::FilterFunction filter)
+ : name(name)
+ , version(version)
+ , summary(summary)
+ , instance(instance)
+ , mcInstance(dynamic_cast<MinecraftInstance*>(instance.get()))
+ , gameRoot(instance->gameRoot())
+ , output(output)
+ , filter(filter)
+{}
+
+void ModrinthPackExportTask::executeTask()
+{
+ setStatus(tr("Searching for files..."));
+ setProgress(0, 0);
+ collectFiles();
+}
+
+bool ModrinthPackExportTask::abort()
+{
+ if (task != nullptr) {
+ task->abort();
+ task = nullptr;
+ emitAborted();
+ return true;
+ }
+
+ if (buildZipFuture.isRunning()) {
+ buildZipFuture.cancel();
+ // NOTE: Here we don't do `emitAborted()` because it will be done when `buildZipFuture` actually cancels, which may not occur
+ // immediately.
+ return true;
+ }
+
+ return false;
+}
+
+void ModrinthPackExportTask::collectFiles()
+{
+ setAbortable(false);
+ QCoreApplication::processEvents();
+
+ files.clear();
+ if (!MMCZip::collectFileListRecursively(instance->gameRoot(), nullptr, &files, filter)) {
+ emitFailed(tr("Could not search for files"));
+ return;
+ }
+
+ pendingHashes.clear();
+ resolvedFiles.clear();
+
+ if (mcInstance) {
+ mcInstance->loaderModList()->update();
+ connect(mcInstance->loaderModList().get(), &ModFolderModel::updateFinished, this, &ModrinthPackExportTask::collectHashes);
+ } else
+ collectHashes();
+}
+
+void ModrinthPackExportTask::collectHashes()
+{
+ setStatus(tr("Finding file hashes..."));
+ for (const QFileInfo& file : files) {
+ QCoreApplication::processEvents();
+
+ const QString relative = gameRoot.relativeFilePath(file.absoluteFilePath());
+ // require sensible file types
+ if (!std::any_of(PREFIXES.begin(), PREFIXES.end(), [&relative](const QString& prefix) { return relative.startsWith(prefix); }))
+ continue;
+ if (!std::any_of(FILE_EXTENSIONS.begin(), FILE_EXTENSIONS.end(), [&relative](const QString& extension) {
+ return relative.endsWith('.' + extension) || relative.endsWith('.' + extension + ".disabled");
+ }))
+ continue;
+
+ QCryptographicHash sha512(QCryptographicHash::Algorithm::Sha512);
+
+ QFile openFile(file.absoluteFilePath());
+ if (!openFile.open(QFile::ReadOnly)) {
+ qWarning() << "Could not open" << file << "for hashing";
+ continue;
+ }
+
+ const QByteArray data = openFile.readAll();
+ if (openFile.error() != QFileDevice::NoError) {
+ qWarning() << "Could not read" << file;
+ continue;
+ }
+ sha512.addData(data);
+
+ auto allMods = mcInstance->loaderModList()->allMods();
+ if (auto modIter = std::find_if(allMods.begin(), allMods.end(), [&file](Mod* mod) { return mod->fileinfo() == file; });
+ modIter != allMods.end()) {
+ const Mod* mod = *modIter;
+ if (mod->metadata() != nullptr) {
+ QUrl& url = mod->metadata()->url;
+ // ensure the url is permitted on modrinth.com
+ if (!url.isEmpty() && BuildConfig.MODRINTH_MRPACK_HOSTS.contains(url.host())) {
+ qDebug() << "Resolving" << relative << "from index";
+
+ QCryptographicHash sha1(QCryptographicHash::Algorithm::Sha1);
+ sha1.addData(data);
+
+ ResolvedFile resolvedFile{ sha1.result().toHex(), sha512.result().toHex(), url.toEncoded(), openFile.size() };
+ resolvedFiles[relative] = resolvedFile;
+
+ // nice! we've managed to resolve based on local metadata!
+ // no need to enqueue it
+ continue;
+ }
+ }
+ }
+
+ qDebug() << "Enqueueing" << relative << "for Modrinth query";
+ pendingHashes[relative] = sha512.result().toHex();
+ }
+
+ setAbortable(true);
+ makeApiRequest();
+}
+
+void ModrinthPackExportTask::makeApiRequest()
+{
+ if (pendingHashes.isEmpty())
+ buildZip();
+ else {
+ setStatus(tr("Finding versions for hashes..."));
+ auto response = std::make_shared<QByteArray>();
+ task = api.currentVersions(pendingHashes.values(), "sha512", response);
+ connect(task.get(), &NetJob::succeeded, [this, response]() { parseApiResponse(response); });
+ connect(task.get(), &NetJob::failed, this, &ModrinthPackExportTask::emitFailed);
+ task->start();
+ }
+}
+
+void ModrinthPackExportTask::parseApiResponse(const std::shared_ptr<QByteArray> response)
+{
+ task = nullptr;
+
+ try {
+ const QJsonDocument doc = Json::requireDocument(*response);
+
+ QMapIterator<QString, QString> iterator(pendingHashes);
+ while (iterator.hasNext()) {
+ iterator.next();
+
+ const QJsonObject obj = doc[iterator.value()].toObject();
+ if (obj.isEmpty())
+ continue;
+
+ const QJsonArray files = obj["files"].toArray();
+ if (auto fileIter = std::find_if(files.begin(), files.end(),
+ [&iterator](const QJsonValue& file) { return file["hashes"]["sha512"] == iterator.value(); });
+ fileIter != files.end()) {
+ // map the file to the url
+ resolvedFiles[iterator.key()] =
+ ResolvedFile{ fileIter->toObject()["hashes"].toObject()["sha1"].toString(), iterator.value(),
+ fileIter->toObject()["url"].toString(), fileIter->toObject()["size"].toInt() };
+ }
+ }
+ } catch (const Json::JsonException& e) {
+ emitFailed(tr("Failed to parse versions response: %1").arg(e.what()));
+ return;
+ }
+ pendingHashes.clear();
+ buildZip();
+}
+
+void ModrinthPackExportTask::buildZip()
+{
+ setStatus(tr("Adding files..."));
+
+ buildZipFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this]() {
+ QuaZip zip(output);
+ if (!zip.open(QuaZip::mdCreate)) {
+ QFile::remove(output);
+ return BuildZipResult(tr("Could not create file"));
+ }
+
+ if (buildZipFuture.isCanceled())
+ return BuildZipResult();
+
+ QuaZipFile indexFile(&zip);
+ if (!indexFile.open(QIODevice::WriteOnly, QuaZipNewInfo("modrinth.index.json"))) {
+ QFile::remove(output);
+ return BuildZipResult(tr("Could not create index"));
+ }
+ indexFile.write(generateIndex());
+
+ size_t progress = 0;
+ for (const QFileInfo& file : files) {
+ if (buildZipFuture.isCanceled()) {
+ QFile::remove(output);
+ return BuildZipResult();
+ }
+
+ setProgress(progress, files.length());
+ const QString relative = gameRoot.relativeFilePath(file.absoluteFilePath());
+ if (!resolvedFiles.contains(relative) && !JlCompress::compressFile(&zip, file.absoluteFilePath(), "overrides/" + relative)) {
+ QFile::remove(output);
+ return BuildZipResult(tr("Could not read and compress %1").arg(relative));
+ }
+ progress++;
+ }
+
+ zip.close();
+
+ if (zip.getZipError() != 0) {
+ QFile::remove(output);
+ return BuildZipResult(tr("A zip error occurred"));
+ }
+
+ return BuildZipResult();
+ });
+ connect(&buildZipWatcher, &QFutureWatcher<BuildZipResult>::finished, this, &ModrinthPackExportTask::finish);
+ buildZipWatcher.setFuture(buildZipFuture);
+}
+
+void ModrinthPackExportTask::finish()
+{
+ if (buildZipFuture.isCanceled())
+ emitAborted();
+ else {
+ const BuildZipResult result = buildZipFuture.result();
+ if (result.has_value())
+ emitFailed(result.value());
+ else
+ emitSucceeded();
+ }
+}
+
+QByteArray ModrinthPackExportTask::generateIndex()
+{
+ QJsonObject out;
+ out["formatVersion"] = 1;
+ out["game"] = "minecraft";
+ out["name"] = name;
+ out["versionId"] = version;
+ if (!summary.isEmpty())
+ out["summary"] = summary;
+
+ if (mcInstance) {
+ auto profile = mcInstance->getPackProfile();
+ // collect all supported components
+ const ComponentPtr minecraft = profile->getComponent("net.minecraft");
+ const ComponentPtr quilt = profile->getComponent("org.quiltmc.quilt-loader");
+ const ComponentPtr fabric = profile->getComponent("net.fabricmc.fabric-loader");
+ const ComponentPtr forge = profile->getComponent("net.minecraftforge");
+
+ // convert all available components to mrpack dependencies
+ QJsonObject dependencies;
+ if (minecraft != nullptr)
+ dependencies["minecraft"] = minecraft->m_version;
+ if (quilt != nullptr)
+ dependencies["quilt-loader"] = quilt->m_version;
+ if (fabric != nullptr)
+ dependencies["fabric-loader"] = fabric->m_version;
+ if (forge != nullptr)
+ dependencies["forge"] = forge->m_version;
+
+ out["dependencies"] = dependencies;
+ }
+
+ QJsonArray filesOut;
+ for (auto iterator = resolvedFiles.constBegin(); iterator != resolvedFiles.constEnd(); iterator++) {
+ QJsonObject fileOut;
+
+ QString path = iterator.key();
+ const ResolvedFile& value = iterator.value();
+
+ // detect disabled mod
+ const QFileInfo pathInfo(path);
+ if (pathInfo.suffix() == "disabled") {
+ // rename it
+ path = pathInfo.dir().filePath(pathInfo.completeBaseName());
+ // ...and make it optional
+ QJsonObject env;
+ env["client"] = "optional";
+ env["server"] = "optional";
+ fileOut["env"] = env;
+ }
+
+ fileOut["path"] = path;
+ fileOut["downloads"] = QJsonArray{ iterator.value().url };
+
+ QJsonObject hashes;
+ hashes["sha1"] = value.sha1;
+ hashes["sha512"] = value.sha512;
+ fileOut["hashes"] = hashes;
+
+ fileOut["fileSize"] = value.size;
+ filesOut << fileOut;
+ }
+ out["files"] = filesOut;
+
+ return QJsonDocument(out).toJson(QJsonDocument::Compact);
+}
diff --git a/launcher/modplatform/modrinth/ModrinthPackExportTask.h b/launcher/modplatform/modrinth/ModrinthPackExportTask.h
new file mode 100644
index 00000000..96f292c1
--- /dev/null
+++ b/launcher/modplatform/modrinth/ModrinthPackExportTask.h
@@ -0,0 +1,77 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2023 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 <QFuture>
+#include <QFutureWatcher>
+#include "BaseInstance.h"
+#include "MMCZip.h"
+#include "minecraft/MinecraftInstance.h"
+#include "modplatform/modrinth/ModrinthAPI.h"
+#include "tasks/Task.h"
+
+class ModrinthPackExportTask : public Task {
+ public:
+ ModrinthPackExportTask(const QString& name,
+ const QString& version,
+ const QString& summary,
+ InstancePtr instance,
+ const QString& output,
+ MMCZip::FilterFunction filter);
+
+ protected:
+ void executeTask() override;
+ bool abort() override;
+
+ private:
+ struct ResolvedFile {
+ QString sha1, sha512, url;
+ qint64 size;
+ };
+
+ static const QStringList PREFIXES;
+ static const QStringList FILE_EXTENSIONS;
+
+ // inputs
+ const QString name, version, summary;
+ const InstancePtr instance;
+ MinecraftInstance* mcInstance;
+ const QDir gameRoot;
+ const QString output;
+ const MMCZip::FilterFunction filter;
+
+ typedef std::optional<QString> BuildZipResult;
+
+ ModrinthAPI api;
+ QFileInfoList files;
+ QMap<QString, QString> pendingHashes;
+ QMap<QString, ResolvedFile> resolvedFiles;
+ Task::Ptr task;
+ QFuture<BuildZipResult> buildZipFuture;
+ QFutureWatcher<BuildZipResult> buildZipWatcher;
+
+ void collectFiles();
+ void collectHashes();
+ void makeApiRequest();
+ void parseApiResponse(const std::shared_ptr<QByteArray> response);
+ void buildZip();
+ void finish();
+
+ QByteArray generateIndex();
+};
diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp
index 7ade131e..b4037349 100644
--- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp
+++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp
@@ -22,7 +22,7 @@
#include "Json.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
-#include "net/NetJob.h"
+#include "modplatform/ModIndex.h"
static ModrinthAPI api;
static ModPlatform::ProviderCapabilities ProviderCaps;
@@ -140,6 +140,28 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject& obj, QString preferred_hash_t
file.version_number = Json::requireString(obj, "version_number");
file.changelog = Json::requireString(obj, "changelog");
+ auto dependencies = Json::ensureArray(obj, "dependencies");
+ for (auto d : dependencies) {
+ auto dep = Json::ensureObject(d);
+ ModPlatform::Dependency dependency;
+ dependency.addonId = Json::ensureString(dep, "project_id");
+ dependency.version = Json::ensureString(dep, "version_id");
+ auto depType = Json::requireString(dep, "dependency_type");
+
+ if (depType == "required")
+ dependency.type = ModPlatform::DependencyType::REQUIRED;
+ else if (depType == "optional")
+ dependency.type = ModPlatform::DependencyType::OPTIONAL;
+ else if (depType == "incompatible")
+ dependency.type = ModPlatform::DependencyType::INCOMPATIBLE;
+ else if (depType == "embedded")
+ dependency.type = ModPlatform::DependencyType::EMBEDDED;
+ else
+ dependency.type = ModPlatform::DependencyType::UNKNOWN;
+
+ file.dependencies.append(dependency);
+ }
+
auto files = Json::requireArray(obj, "files");
int i = 0;
@@ -195,3 +217,22 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject& obj, QString preferred_hash_t
return {};
}
+
+auto Modrinth::loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion
+{
+ QVector<ModPlatform::IndexedVersion> versions;
+
+ for (auto versionIter : arr) {
+ auto obj = versionIter.toObject();
+ auto file = loadIndexedPackVersion(obj);
+
+ if (file.fileId.isValid()) // Heuristic to check if the returned value is valid
+ versions.append(file);
+ }
+ auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool {
+ // dates are in RFC 3339 format
+ return a.date > b.date;
+ };
+ std::sort(versions.begin(), versions.end(), orderSortPredicate);
+ return versions.length() != 0 ? versions.front() : ModPlatform::IndexedVersion();
+} \ No newline at end of file
diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.h b/launcher/modplatform/modrinth/ModrinthPackIndex.h
index e73e4b18..a8d986c5 100644
--- a/launcher/modplatform/modrinth/ModrinthPackIndex.h
+++ b/launcher/modplatform/modrinth/ModrinthPackIndex.h
@@ -19,8 +19,8 @@
#include "modplatform/ModIndex.h"
-#include "BaseInstance.h"
#include <QNetworkAccessManager>
+#include "BaseInstance.h"
namespace Modrinth {
@@ -31,5 +31,6 @@ void loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
const shared_qobject_ptr<QNetworkAccessManager>& network,
const BaseInstance* inst);
auto loadIndexedPackVersion(QJsonObject& obj, QString hash_type = "sha512", QString filename_prefer = "") -> ModPlatform::IndexedVersion;
+auto loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion;
} // namespace Modrinth
diff --git a/launcher/modplatform/technic/SingleZipPackInstallTask.cpp b/launcher/modplatform/technic/SingleZipPackInstallTask.cpp
index 8fd43d21..f07ca24a 100644
--- a/launcher/modplatform/technic/SingleZipPackInstallTask.cpp
+++ b/launcher/modplatform/technic/SingleZipPackInstallTask.cpp
@@ -50,6 +50,7 @@ void Technic::SingleZipPackInstallTask::executeTask()
auto job = m_filesNetJob.get();
connect(job, &NetJob::succeeded, this, &Technic::SingleZipPackInstallTask::downloadSucceeded);
connect(job, &NetJob::progress, this, &Technic::SingleZipPackInstallTask::downloadProgressChanged);
+ connect(job, &NetJob::stepProgress, this, &Technic::SingleZipPackInstallTask::propogateStepProgress);
connect(job, &NetJob::failed, this, &Technic::SingleZipPackInstallTask::downloadFailed);
m_filesNetJob->start();
}
diff --git a/launcher/modplatform/technic/SolderPackInstallTask.cpp b/launcher/modplatform/technic/SolderPackInstallTask.cpp
index 77c503f0..6a05d17a 100644
--- a/launcher/modplatform/technic/SolderPackInstallTask.cpp
+++ b/launcher/modplatform/technic/SolderPackInstallTask.cpp
@@ -37,20 +37,19 @@
#include <FileSystem.h>
#include <Json.h>
-#include <QtConcurrentRun>
#include <MMCZip.h>
+#include <QtConcurrentRun>
-#include "TechnicPackProcessor.h"
#include "SolderPackManifest.h"
+#include "TechnicPackProcessor.h"
#include "net/ChecksumValidator.h"
-Technic::SolderPackInstallTask::SolderPackInstallTask(
- shared_qobject_ptr<QNetworkAccessManager> network,
- const QUrl &solderUrl,
- const QString &pack,
- const QString &version,
- const QString &minecraftVersion
-) {
+Technic::SolderPackInstallTask::SolderPackInstallTask(shared_qobject_ptr<QNetworkAccessManager> network,
+ const QUrl& solderUrl,
+ const QString& pack,
+ const QString& version,
+ const QString& minecraftVersion)
+{
m_solderUrl = solderUrl;
m_pack = pack;
m_version = version;
@@ -58,9 +57,9 @@ Technic::SolderPackInstallTask::SolderPackInstallTask(
m_minecraftVersion = minecraftVersion;
}
-bool Technic::SolderPackInstallTask::abort() {
- if(m_abortable)
- {
+bool Technic::SolderPackInstallTask::abort()
+{
+ if (m_abortable) {
return m_filesNetJob->abort();
}
return false;
@@ -72,7 +71,7 @@ void Technic::SolderPackInstallTask::executeTask()
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));
+ m_filesNetJob->addNetAction(Net::Download::makeByteArray(sourceUrl, m_response));
auto job = m_filesNetJob.get();
connect(job, &NetJob::succeeded, this, &Technic::SolderPackInstallTask::fileListSucceeded);
@@ -85,11 +84,11 @@ void Technic::SolderPackInstallTask::fileListSucceeded()
{
setStatus(tr("Downloading modpack"));
- QJsonParseError parse_error {};
- QJsonDocument doc = QJsonDocument::fromJson(m_response, &parse_error);
+ QJsonParseError parse_error{};
+ QJsonDocument doc = QJsonDocument::fromJson(*m_response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from Solder at " << parse_error.offset << " reason: " << parse_error.errorString();
- qWarning() << m_response;
+ qWarning() << *m_response;
return;
}
auto obj = doc.object();
@@ -110,7 +109,7 @@ void Technic::SolderPackInstallTask::fileListSucceeded()
m_filesNetJob.reset(new NetJob(tr("Downloading modpack"), m_network));
int i = 0;
- for (const auto &mod : build.mods) {
+ for (const auto& mod : build.mods) {
auto path = FS::PathCombine(m_outputDir.path(), QString("%1").arg(i));
auto dl = Net::Download::makeFile(mod.url, path);
@@ -127,6 +126,7 @@ void Technic::SolderPackInstallTask::fileListSucceeded()
connect(m_filesNetJob.get(), &NetJob::succeeded, this, &Technic::SolderPackInstallTask::downloadSucceeded);
connect(m_filesNetJob.get(), &NetJob::progress, this, &Technic::SolderPackInstallTask::downloadProgressChanged);
+ connect(m_filesNetJob.get(), &NetJob::stepProgress, this, &Technic::SolderPackInstallTask::propogateStepProgress);
connect(m_filesNetJob.get(), &NetJob::failed, this, &Technic::SolderPackInstallTask::downloadFailed);
connect(m_filesNetJob.get(), &NetJob::aborted, this, &Technic::SolderPackInstallTask::downloadAborted);
m_filesNetJob->start();
diff --git a/launcher/modplatform/technic/SolderPackInstallTask.h b/launcher/modplatform/technic/SolderPackInstallTask.h
index aa14ce88..f2c6a83a 100644
--- a/launcher/modplatform/technic/SolderPackInstallTask.h
+++ b/launcher/modplatform/technic/SolderPackInstallTask.h
@@ -40,45 +40,48 @@
#include <tasks/Task.h>
#include <QUrl>
+#include <memory>
-namespace Technic
-{
- class SolderPackInstallTask : public InstanceTask
- {
- Q_OBJECT
- public:
- explicit SolderPackInstallTask(shared_qobject_ptr<QNetworkAccessManager> network, const QUrl &solderUrl, const QString& pack, const QString& version, const QString &minecraftVersion);
+namespace Technic {
+class SolderPackInstallTask : public InstanceTask {
+ Q_OBJECT
+ public:
+ explicit SolderPackInstallTask(shared_qobject_ptr<QNetworkAccessManager> network,
+ const QUrl& solderUrl,
+ const QString& pack,
+ const QString& version,
+ const QString& minecraftVersion);
- bool canAbort() const override { return true; }
- bool abort() override;
+ bool canAbort() const override { return true; }
+ bool abort() override;
- protected:
- //! Entry point for tasks.
- virtual void executeTask() override;
+ protected:
+ //! Entry point for tasks.
+ virtual void executeTask() override;
- private slots:
- void fileListSucceeded();
- void downloadSucceeded();
- void downloadFailed(QString reason);
- void downloadProgressChanged(qint64 current, qint64 total);
- void downloadAborted();
- void extractFinished();
- void extractAborted();
+ private slots:
+ void fileListSucceeded();
+ void downloadSucceeded();
+ void downloadFailed(QString reason);
+ void downloadProgressChanged(qint64 current, qint64 total);
+ void downloadAborted();
+ void extractFinished();
+ void extractAborted();
- private:
- bool m_abortable = false;
+ private:
+ bool m_abortable = false;
- shared_qobject_ptr<QNetworkAccessManager> m_network;
+ shared_qobject_ptr<QNetworkAccessManager> m_network;
- NetJob::Ptr m_filesNetJob;
- QUrl m_solderUrl;
- QString m_pack;
- QString m_version;
- QString m_minecraftVersion;
- QByteArray m_response;
- QTemporaryDir m_outputDir;
- int m_modCount;
- QFuture<bool> m_extractFuture;
- QFutureWatcher<bool> m_extractFutureWatcher;
- };
-}
+ NetJob::Ptr m_filesNetJob;
+ QUrl m_solderUrl;
+ QString m_pack;
+ QString m_version;
+ QString m_minecraftVersion;
+ std::shared_ptr<QByteArray> m_response = std::make_shared<QByteArray>();
+ QTemporaryDir m_outputDir;
+ int m_modCount;
+ QFuture<bool> m_extractFuture;
+ QFutureWatcher<bool> m_extractFutureWatcher;
+};
+} // namespace Technic
diff --git a/launcher/mojang/PackageManifest.cpp b/launcher/mojang/PackageManifest.cpp
deleted file mode 100644
index b3dfd7fc..00000000
--- a/launcher/mojang/PackageManifest.cpp
+++ /dev/null
@@ -1,427 +0,0 @@
-#include "PackageManifest.h"
-#include <Json.h>
-#include <QDir>
-#include <QDirIterator>
-#include <QCryptographicHash>
-#include <QDebug>
-
-#ifndef Q_OS_WIN32
-#include <unistd.h>
-#include <sys/types.h>
-#include <sys/stat.h>
-#endif
-
-namespace mojang_files {
-
-const Hash hash_of_empty_string = "da39a3ee5e6b4b0d3255bfef95601890afd80709";
-
-int Path::compare(const Path& rhs) const
-{
- auto left_cursor = begin();
- auto left_end = end();
- auto right_cursor = rhs.begin();
- auto right_end = rhs.end();
-
- while (left_cursor != left_end && right_cursor != right_end)
- {
- if(*left_cursor < *right_cursor)
- {
- return -1;
- }
- else if(*left_cursor > *right_cursor)
- {
- return 1;
- }
- left_cursor++;
- right_cursor++;
- }
-
- if(left_cursor == left_end)
- {
- if(right_cursor == right_end)
- {
- return 0;
- }
- return -1;
- }
- return 1;
-}
-
-void Package::addFile(const Path& path, const File& file) {
- addFolder(path.parent_path());
- files[path] = file;
-}
-
-void Package::addFolder(Path folder) {
- if(!folder.has_parent_path()) {
- return;
- }
- do {
- folders.insert(folder);
- folder = folder.parent_path();
- } while(folder.has_parent_path());
-}
-
-void Package::addLink(const Path& path, const Path& target) {
- addFolder(path.parent_path());
- symlinks[path] = target;
-}
-
-void Package::addSource(const FileSource& source) {
- sources[source.hash] = source;
-}
-
-
-namespace {
-void fromJson(QJsonDocument & doc, Package & out) {
- std::set<Path> seen_paths;
- if (!doc.isObject())
- {
- throw JSONValidationError("file manifest is not an object");
- }
- QJsonObject root = doc.object();
-
- auto filesObj = Json::ensureObject(root, "files");
- auto iter = filesObj.begin();
- while (iter != filesObj.end())
- {
- Path objectPath = Path(iter.key());
- auto value = iter.value();
- iter++;
- if(seen_paths.count(objectPath)) {
- throw JSONValidationError("duplicate path inside manifest, the manifest is invalid");
- }
- if (!value.isObject())
- {
- throw JSONValidationError("file entry inside manifest is not an an object");
- }
- seen_paths.insert(objectPath);
-
- auto fileObject = value.toObject();
- auto type = Json::requireString(fileObject, "type");
- if(type == "directory") {
- out.addFolder(objectPath);
- continue;
- }
- else if(type == "file") {
- FileSource bestSource;
- File file;
- file.executable = Json::ensureBoolean(fileObject, QString("executable"), false);
- auto downloads = Json::requireObject(fileObject, "downloads");
- for(auto iter2 = downloads.begin(); iter2 != downloads.end(); iter2++) {
- FileSource source;
-
- auto downloadObject = Json::requireObject(iter2.value());
- source.hash = Json::requireString(downloadObject, "sha1");
- source.size = Json::requireInteger(downloadObject, "size");
- source.url = Json::requireString(downloadObject, "url");
-
- auto compression = iter2.key();
- if(compression == "raw") {
- file.hash = source.hash;
- file.size = source.size;
- source.compression = Compression::Raw;
- }
- else if (compression == "lzma") {
- source.compression = Compression::Lzma;
- }
- else {
- continue;
- }
- bestSource.upgrade(source);
- }
- if(bestSource.isBad()) {
- throw JSONValidationError("No valid compression method for file " + iter.key());
- }
- out.addFile(objectPath, file);
- out.addSource(bestSource);
- }
- else if(type == "link") {
- auto target = Json::requireString(fileObject, "target");
- out.symlinks[objectPath] = target;
- out.addLink(objectPath, target);
- }
- else {
- throw JSONValidationError("Invalid item type in manifest: " + type);
- }
- }
- // make sure the containing folder exists
- out.folders.insert(Path());
-}
-}
-
-Package Package::fromManifestContents(const QByteArray& contents)
-{
- Package out;
- try
- {
- auto doc = Json::requireDocument(contents, "Manifest");
- fromJson(doc, out);
- return out;
- }
- catch (const Exception &e)
- {
- qDebug() << QString("Unable to parse manifest: %1").arg(e.cause());
- out.valid = false;
- return out;
- }
-}
-
-Package Package::fromManifestFile(const QString & filename) {
- Package out;
- try
- {
- auto doc = Json::requireDocument(filename, filename);
- fromJson(doc, out);
- return out;
- }
- catch (const Exception &e)
- {
- qDebug() << QString("Unable to parse manifest file %1: %2").arg(filename, e.cause());
- out.valid = false;
- return out;
- }
-}
-
-#ifndef Q_OS_WIN32
-
-#include <unistd.h>
-#include <sys/types.h>
-#include <sys/stat.h>
-
-namespace {
-// FIXME: Qt obscures symlink targets by making them absolute. that is useless. this is the workaround - we do it ourselves
-bool actually_read_symlink_target(const QString & filepath, Path & out)
-{
- struct ::stat st;
- // FIXME: here, we assume the native filesystem encoding. May the Gods have mercy upon our Souls.
- QByteArray nativePath = filepath.toUtf8();
- const char * filepath_cstr = nativePath.data();
-
- if (lstat(filepath_cstr, &st) != 0)
- {
- return false;
- }
-
- auto size = st.st_size ? st.st_size + 1 : PATH_MAX;
- std::string temp(size, '\0');
- // because we don't realiably know how long the damn thing actually is, we loop and expand. POSIX is naff
- do
- {
- auto link_length = ::readlink(filepath_cstr, &temp[0], temp.size());
- if(link_length == -1)
- {
- return false;
- }
- if(std::string::size_type(link_length) < temp.size())
- {
- // buffer was long enough and we managed to read the link target. RETURN here.
- temp.resize(link_length);
- out = Path(QString::fromUtf8(temp.c_str()));
- return true;
- }
- temp.resize(temp.size() * 2);
- } while (true);
-}
-}
-#endif
-
-// FIXME: Qt filesystem abstraction is bad, but ... let's hope it doesn't break too much?
-// FIXME: The error handling is just DEFICIENT
-Package Package::fromInspectedFolder(const QString& folderPath)
-{
- QDir root(folderPath);
-
- Package out;
- QDirIterator iterator(folderPath, QDir::NoDotAndDotDot | QDir::AllEntries | QDir::System | QDir::Hidden, QDirIterator::Subdirectories);
- while(iterator.hasNext()) {
- iterator.next();
-
- auto fileInfo = iterator.fileInfo();
- auto relPath = root.relativeFilePath(fileInfo.filePath());
- // FIXME: this is probably completely busted on Windows anyway, so just disable it.
- // Qt makes shit up and doesn't understand the platform details
- // TODO: Actually use a filesystem library that isn't terrible and has decen license.
- // I only know one, and I wrote it. Sadly, currently proprietary. PAIN.
-#ifndef Q_OS_WIN32
- if(fileInfo.isSymLink()) {
- Path targetPath;
- if(!actually_read_symlink_target(fileInfo.filePath(), targetPath)) {
- qCritical() << "Folder inspection: Unknown filesystem object:" << fileInfo.absoluteFilePath();
- out.valid = false;
- }
- out.addLink(relPath, targetPath);
- }
- else
-#endif
- if(fileInfo.isDir()) {
- out.addFolder(relPath);
- }
- else if(fileInfo.isFile()) {
- File f;
- f.executable = fileInfo.isExecutable();
- f.size = fileInfo.size();
- // FIXME: async / optimize the hashing
- QFile input(fileInfo.absoluteFilePath());
- if(!input.open(QIODevice::ReadOnly)) {
- qCritical() << "Folder inspection: Failed to open file:" << fileInfo.absoluteFilePath();
- out.valid = false;
- break;
- }
- f.hash = QCryptographicHash::hash(input.readAll(), QCryptographicHash::Sha1).toHex().constData();
- out.addFile(relPath, f);
- }
- else {
- // Something else... oh my
- qCritical() << "Folder inspection: Unknown filesystem object:" << fileInfo.absoluteFilePath();
- out.valid = false;
- break;
- }
- }
- out.folders.insert(Path("."));
- out.valid = true;
- return out;
-}
-
-namespace {
-struct shallow_first_sort
-{
- bool operator()(const Path &lhs, const Path &rhs) const
- {
- auto lhs_depth = lhs.length();
- auto rhs_depth = rhs.length();
- if(lhs_depth < rhs_depth)
- {
- return true;
- }
- else if(lhs_depth == rhs_depth)
- {
- if(lhs < rhs)
- {
- return true;
- }
- }
- return false;
- }
-};
-
-struct deep_first_sort
-{
- bool operator()(const Path &lhs, const Path &rhs) const
- {
- auto lhs_depth = lhs.length();
- auto rhs_depth = rhs.length();
- if(lhs_depth > rhs_depth)
- {
- return true;
- }
- else if(lhs_depth == rhs_depth)
- {
- if(lhs < rhs)
- {
- return true;
- }
- }
- return false;
- }
-};
-}
-
-UpdateOperations UpdateOperations::resolve(const Package& from, const Package& to)
-{
- UpdateOperations out;
-
- if(!from.valid || !to.valid) {
- out.valid = false;
- return out;
- }
-
- // Files
- for(auto iter = from.files.begin(); iter != from.files.end(); iter++) {
- const auto &current_hash = iter->second.hash;
- const auto &current_executable = iter->second.executable;
- const auto &path = iter->first;
-
- auto iter2 = to.files.find(path);
- if(iter2 == to.files.end()) {
- // removed
- out.deletes.push_back(path);
- continue;
- }
- auto new_hash = iter2->second.hash;
- auto new_executable = iter2->second.executable;
- if (current_hash != new_hash) {
- out.deletes.push_back(path);
- out.downloads.emplace(
- std::pair<Path, FileDownload>{
- path,
- FileDownload(to.sources.at(iter2->second.hash), iter2->second.executable)
- }
- );
- }
- else if (current_executable != new_executable) {
- out.executable_fixes[path] = new_executable;
- }
- }
- for(auto iter = to.files.begin(); iter != to.files.end(); iter++) {
- auto path = iter->first;
- if(!from.files.count(path)) {
- out.downloads.emplace(
- std::pair<Path, FileDownload>{
- path,
- FileDownload(to.sources.at(iter->second.hash), iter->second.executable)
- }
- );
- }
- }
-
- // Folders
- std::set<Path, deep_first_sort> remove_folders;
- std::set<Path, shallow_first_sort> make_folders;
- for(auto from_path: from.folders) {
- auto iter = to.folders.find(from_path);
- if(iter == to.folders.end()) {
- remove_folders.insert(from_path);
- }
- }
- for(auto & rmdir: remove_folders) {
- out.rmdirs.push_back(rmdir);
- }
- for(auto to_path: to.folders) {
- auto iter = from.folders.find(to_path);
- if(iter == from.folders.end()) {
- make_folders.insert(to_path);
- }
- }
- for(auto & mkdir: make_folders) {
- out.mkdirs.push_back(mkdir);
- }
-
- // Symlinks
- for(auto iter = from.symlinks.begin(); iter != from.symlinks.end(); iter++) {
- const auto &current_target = iter->second;
- const auto &path = iter->first;
-
- auto iter2 = to.symlinks.find(path);
- if(iter2 == to.symlinks.end()) {
- // removed
- out.deletes.push_back(path);
- continue;
- }
- const auto &new_target = iter2->second;
- if (current_target != new_target) {
- out.deletes.push_back(path);
- out.mklinks[path] = iter2->second;
- }
- }
- for(auto iter = to.symlinks.begin(); iter != to.symlinks.end(); iter++) {
- auto path = iter->first;
- if(!from.symlinks.count(path)) {
- out.mklinks[path] = iter->second;
- }
- }
- out.valid = true;
- return out;
-}
-
-}
diff --git a/launcher/mojang/PackageManifest.h b/launcher/mojang/PackageManifest.h
deleted file mode 100644
index fd7ab0ad..00000000
--- a/launcher/mojang/PackageManifest.h
+++ /dev/null
@@ -1,171 +0,0 @@
-#pragma once
-
-#include <QString>
-#include <map>
-#include <set>
-#include <QStringList>
-#include "tasks/Task.h"
-
-namespace mojang_files {
-
-using Hash = QString;
-extern const Hash empty_hash;
-
-// simple-ish path implementation. assumes always relative and does not allow '..' entries
-class Path
-{
-public:
- using parts_type = QStringList;
-
- Path() = default;
- Path(QString string) {
- auto parts_in = string.split('/');
- for(auto & part: parts_in) {
- if(part.isEmpty() || part == ".") {
- continue;
- }
- if(part == "..") {
- if(parts.size()) {
- parts.pop_back();
- }
- continue;
- }
- parts.push_back(part);
- }
- }
-
- bool has_parent_path() const
- {
- return parts.size() > 0;
- }
-
- Path parent_path() const
- {
- if (parts.empty())
- return Path();
- return Path(parts.begin(), std::prev(parts.end()));
- }
-
- bool empty() const
- {
- return parts.empty();
- }
-
- int length() const
- {
- return parts.length();
- }
-
- bool operator==(const Path & rhs) const {
- return parts == rhs.parts;
- }
-
- bool operator!=(const Path & rhs) const {
- return parts != rhs.parts;
- }
-
- inline bool operator<(const Path& rhs) const
- {
- return compare(rhs) < 0;
- }
-
- parts_type::const_iterator begin() const
- {
- return parts.begin();
- }
-
- parts_type::const_iterator end() const
- {
- return parts.end();
- }
-
- QString toString() const {
- return parts.join("/");
- }
-
-private:
- Path(const parts_type::const_iterator & start, const parts_type::const_iterator & end) {
- auto cursor = start;
- while(cursor != end) {
- parts.push_back(*cursor);
- cursor++;
- }
- }
- int compare(const Path& p) const;
-
- parts_type parts;
-};
-
-
-enum class Compression {
- Raw,
- Lzma,
- Unknown
-};
-
-
-struct FileSource
-{
- Compression compression = Compression::Unknown;
- Hash hash;
- QString url;
- std::size_t size = 0;
- void upgrade(const FileSource & other) {
- if(compression == Compression::Unknown || other.size < size) {
- *this = other;
- }
- }
- bool isBad() const {
- return compression == Compression::Unknown;
- }
-};
-
-struct File
-{
- Hash hash;
- bool executable;
- std::uint64_t size = 0;
-};
-
-struct Package {
- static Package fromInspectedFolder(const QString &folderPath);
- static Package fromManifestFile(const QString &path);
- static Package fromManifestContents(const QByteArray& contents);
-
- explicit operator bool() const
- {
- return valid;
- }
- void addFolder(Path folder);
- void addFile(const Path & path, const File & file);
- void addLink(const Path & path, const Path & target);
- void addSource(const FileSource & source);
-
- std::map<Hash, FileSource> sources;
- bool valid = true;
- std::set<Path> folders;
- std::map<Path, File> files;
- std::map<Path, Path> symlinks;
-};
-
-struct FileDownload : FileSource
-{
- FileDownload(const FileSource& source, bool executable) {
- static_cast<FileSource &> (*this) = source;
- this->executable = executable;
- }
- bool executable = false;
-};
-
-struct UpdateOperations {
- static UpdateOperations resolve(const Package & from, const Package & to);
- bool valid = false;
- std::vector<Path> deletes;
- std::vector<Path> rmdirs;
- std::vector<Path> mkdirs;
- std::map<Path, FileDownload> downloads;
- std::map<Path, Path> mklinks;
- std::map<Path, bool> executable_fixes;
-};
-
-}
diff --git a/launcher/net/ByteArraySink.h b/launcher/net/ByteArraySink.h
index 501318a1..d6b17d60 100644
--- a/launcher/net/ByteArraySink.h
+++ b/launcher/net/ByteArraySink.h
@@ -1,7 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
- * PolyMC - Minecraft Launcher
+ * Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
+ * Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -46,14 +47,17 @@ namespace Net {
*/
class ByteArraySink : public Sink {
public:
- ByteArraySink(QByteArray* output) : m_output(output){};
+ ByteArraySink(std::shared_ptr<QByteArray> output) : m_output(output){};
virtual ~ByteArraySink() = default;
public:
auto init(QNetworkRequest& request) -> Task::State override
{
- m_output->clear();
+ if (m_output)
+ m_output->clear();
+ else
+ qWarning() << "ByteArraySink did not initialize the buffer because it's not addressable";
if (initAllValidators(request))
return Task::State::Running;
return Task::State::Failed;
@@ -61,7 +65,10 @@ class ByteArraySink : public Sink {
auto write(QByteArray& data) -> Task::State override
{
- m_output->append(data);
+ if (m_output)
+ m_output->append(data);
+ else
+ qWarning() << "ByteArraySink did not write the buffer because it's not addressable";
if (writeAllValidators(data))
return Task::State::Running;
return Task::State::Failed;
@@ -69,7 +76,10 @@ class ByteArraySink : public Sink {
auto abort() -> Task::State override
{
- m_output->clear();
+ if (m_output)
+ m_output->clear();
+ else
+ qWarning() << "ByteArraySink did not clear the buffer because it's not addressable";
failAllValidators();
return Task::State::Failed;
}
@@ -84,6 +94,6 @@ class ByteArraySink : public Sink {
auto hasLocalData() -> bool override { return false; }
private:
- QByteArray* m_output;
+ std::shared_ptr<QByteArray> m_output;
};
} // namespace Net
diff --git a/launcher/net/Download.cpp b/launcher/net/Download.cpp
index e8a1d0b0..4ea45c63 100644
--- a/launcher/net/Download.cpp
+++ b/launcher/net/Download.cpp
@@ -4,6 +4,7 @@
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
+ * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.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
@@ -36,17 +37,24 @@
*/
#include "Download.h"
+#include <QUrl>
#include <QDateTime>
#include <QFileInfo>
+#include <memory>
#include "ByteArraySink.h"
#include "ChecksumValidator.h"
-#include "FileSystem.h"
#include "MetaCacheSink.h"
-#include "BuildConfig.h"
#include "Application.h"
+#include "BuildConfig.h"
+
+#include "net/Logging.h"
+#include "net/NetAction.h"
+
+#include "MMCTime.h"
+#include "StringUtils.h"
namespace Net {
@@ -54,6 +62,7 @@ auto Download::makeCached(QUrl url, MetaEntryPtr entry, Options options) -> Down
{
auto dl = makeShared<Download>();
dl->m_url = url;
+ dl->setObjectName(QString("CACHE:") + url.toString());
dl->m_options = options;
auto md5Node = new ChecksumValidator(QCryptographicHash::Md5);
auto cachedNode = new MetaCacheSink(entry, md5Node, options.testFlag(Option::MakeEternal));
@@ -61,10 +70,11 @@ auto Download::makeCached(QUrl url, MetaEntryPtr entry, Options options) -> Down
return dl;
}
-auto Download::makeByteArray(QUrl url, QByteArray* output, Options options) -> Download::Ptr
+auto Download::makeByteArray(QUrl url, std::shared_ptr<QByteArray> output, Options options) -> Download::Ptr
{
auto dl = makeShared<Download>();
dl->m_url = url;
+ dl->setObjectName(QString("BYTES:") + url.toString());
dl->m_options = options;
dl->m_sink.reset(new ByteArraySink(output));
return dl;
@@ -74,6 +84,7 @@ auto Download::makeFile(QUrl url, QString path, Options options) -> Download::Pt
{
auto dl = makeShared<Download>();
dl->m_url = url;
+ dl->setObjectName(QString("FILE:") + url.toString());
dl->m_options = options;
dl->m_sink.reset(new FileSink(path));
return dl;
@@ -86,10 +97,10 @@ void Download::addValidator(Validator* v)
void Download::executeTask()
{
- setStatus(tr("Downloading %1").arg(m_url.toString()));
+ setStatus(tr("Downloading %1").arg(StringUtils::truncateUrlHumanFriendly(m_url, 80)));
if (getState() == Task::State::AbortedByUser) {
- qWarning() << "Attempt to start an aborted Download:" << m_url.toString();
+ qCWarning(taskDownloadLogC) << getUid().toString() << "Attempt to start an aborted Download:" << m_url.toString();
emitAborted();
return;
}
@@ -99,10 +110,10 @@ void Download::executeTask()
switch (m_state) {
case State::Succeeded:
emit succeeded();
- qDebug() << "Download cache hit " << m_url.toString();
+ qCDebug(taskDownloadLogC) << getUid().toString() << "Download cache hit " << m_url.toString();
return;
case State::Running:
- qDebug() << "Downloading " << m_url.toString();
+ qCDebug(taskDownloadLogC) << getUid().toString() << "Downloading " << m_url.toString();
break;
case State::Inactive:
case State::Failed:
@@ -124,15 +135,21 @@ void Download::executeTask()
request.setRawHeader("Authorization", token.toUtf8());
}
- QNetworkReply* rep = m_network->get(request);
+#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
+ request.setTransferTimeout();
+#endif
+ m_last_progress_time = m_clock.now();
+ m_last_progress_bytes = 0;
+
+ QNetworkReply* rep = m_network->get(request);
m_reply.reset(rep);
connect(rep, &QNetworkReply::downloadProgress, this, &Download::downloadProgress);
connect(rep, &QNetworkReply::finished, this, &Download::downloadFinished);
-#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
- connect(rep, SIGNAL(errorOccurred(QNetworkReply::NetworkError)), SLOT(downloadError(QNetworkReply::NetworkError)));
+#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15
+ connect(rep, &QNetworkReply::errorOccurred, this, &Download::downloadError);
#else
- connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), SLOT(downloadError(QNetworkReply::NetworkError)));
+ connect(rep, QOverload<QNetworkReply::NetworkError>::of(&QNetworkReply::error), this, &Download::downloadError);
#endif
connect(rep, &QNetworkReply::sslErrors, this, &Download::sslErrors);
connect(rep, &QNetworkReply::readyRead, this, &Download::downloadReadyRead);
@@ -140,13 +157,39 @@ void Download::executeTask()
void Download::downloadProgress(qint64 bytesReceived, qint64 bytesTotal)
{
+ auto now = m_clock.now();
+ auto elapsed = now - m_last_progress_time;
+
+ // use milliseconds for speed precision
+ auto elapsed_ms = std::chrono::duration_cast<std::chrono::milliseconds>(elapsed);
+ auto bytes_received_since = bytesReceived - m_last_progress_bytes;
+ auto dl_speed_bps = (double)bytes_received_since / elapsed_ms.count() * 1000;
+ auto remaing_time_s = (bytesTotal - bytesReceived) / dl_speed_bps;
+
+ //: Current amount of bytes downloaded, out of the total amount of bytes in the download
+ QString dl_progress =
+ tr("%1 / %2").arg(StringUtils::humanReadableFileSize(bytesReceived)).arg(StringUtils::humanReadableFileSize(bytesTotal));
+
+ QString dl_speed_str;
+ if (elapsed_ms.count() > 0) {
+ auto str_eta = bytesTotal > 0 ? Time::humanReadableDuration(remaing_time_s) : tr("unknown");
+ //: Download speed, in bytes per second (remaining download time in parenthesis)
+ dl_speed_str =
+ tr("%1 /s (%2)").arg(StringUtils::humanReadableFileSize(dl_speed_bps)).arg(str_eta);
+ } else {
+ //: Download speed at 0 bytes per second
+ dl_speed_str = tr("0 B/s");
+ }
+
+ setDetails(dl_progress + "\n" + dl_speed_str);
+
setProgress(bytesReceived, bytesTotal);
}
void Download::downloadError(QNetworkReply::NetworkError error)
{
if (error == QNetworkReply::OperationCanceledError) {
- qCritical() << "Aborted " << m_url.toString();
+ qCCritical(taskDownloadLogC) << getUid().toString() << "Aborted " << m_url.toString();
m_state = State::AbortedByUser;
} else {
if (m_options & Option::AcceptLocalFiles) {
@@ -156,7 +199,7 @@ void Download::downloadError(QNetworkReply::NetworkError error)
}
}
// error happened during download.
- qCritical() << "Failed " << m_url.toString() << " with reason " << error;
+ qCCritical(taskDownloadLogC) << getUid().toString() << "Failed " << m_url.toString() << " with reason " << error;
m_state = State::Failed;
}
}
@@ -165,9 +208,10 @@ void Download::sslErrors(const QList<QSslError>& errors)
{
int i = 1;
for (auto error : errors) {
- qCritical() << "Download" << m_url.toString() << "SSL Error #" << i << " : " << error.errorString();
+ qCCritical(taskDownloadLogC) << getUid().toString() << "Download" << m_url.toString() << "SSL Error #" << i << " : "
+ << error.errorString();
auto cert = error.certificate();
- qCritical() << "Certificate in question:\n" << cert.toText();
+ qCCritical(taskDownloadLogC) << getUid().toString() << "Certificate in question:\n" << cert.toText();
i++;
}
}
@@ -210,17 +254,17 @@ auto Download::handleRedirect() -> bool
*/
redirect = QUrl(redirectStr, QUrl::TolerantMode);
if (!redirect.isValid()) {
- qWarning() << "Failed to parse redirect URL:" << redirectStr;
+ qCWarning(taskDownloadLogC) << getUid().toString() << "Failed to parse redirect URL:" << redirectStr;
downloadError(QNetworkReply::ProtocolFailure);
return false;
}
- qDebug() << "Fixed location header:" << redirect;
+ qCDebug(taskDownloadLogC) << getUid().toString() << "Fixed location header:" << redirect;
} else {
- qDebug() << "Location header:" << redirect;
+ qCDebug(taskDownloadLogC) << getUid().toString() << "Location header:" << redirect;
}
m_url = QUrl(redirect.toString());
- qDebug() << "Following redirect to " << m_url.toString();
+ qCDebug(taskDownloadLogC) << getUid().toString() << "Following redirect to " << m_url.toString();
startAction(m_network);
return true;
@@ -230,26 +274,26 @@ void Download::downloadFinished()
{
// handle HTTP redirection first
if (handleRedirect()) {
- qDebug() << "Download redirected:" << m_url.toString();
+ qCDebug(taskDownloadLogC) << getUid().toString() << "Download redirected:" << m_url.toString();
return;
}
// if the download failed before this point ...
if (m_state == State::Succeeded) // pretend to succeed so we continue processing :)
{
- qDebug() << "Download failed but we are allowed to proceed:" << m_url.toString();
+ qCDebug(taskDownloadLogC) << getUid().toString() << "Download failed but we are allowed to proceed:" << m_url.toString();
m_sink->abort();
m_reply.reset();
emit succeeded();
return;
} else if (m_state == State::Failed) {
- qDebug() << "Download failed in previous step:" << m_url.toString();
+ qCDebug(taskDownloadLogC) << getUid().toString() << "Download failed in previous step:" << m_url.toString();
m_sink->abort();
m_reply.reset();
emit failed("");
return;
} else if (m_state == State::AbortedByUser) {
- qDebug() << "Download aborted in previous step:" << m_url.toString();
+ qCDebug(taskDownloadLogC) << getUid().toString() << "Download aborted in previous step:" << m_url.toString();
m_sink->abort();
m_reply.reset();
emit aborted();
@@ -259,14 +303,14 @@ void Download::downloadFinished()
// make sure we got all the remaining data, if any
auto data = m_reply->readAll();
if (data.size()) {
- qDebug() << "Writing extra" << data.size() << "bytes";
+ qCDebug(taskDownloadLogC) << getUid().toString() << "Writing extra" << data.size() << "bytes";
m_state = m_sink->write(data);
}
// otherwise, finalize the whole graph
m_state = m_sink->finalize(*m_reply.get());
if (m_state != State::Succeeded) {
- qDebug() << "Download failed to finalize:" << m_url.toString();
+ qCDebug(taskDownloadLogC) << getUid().toString() << "Download failed to finalize:" << m_url.toString();
m_sink->abort();
m_reply.reset();
emit failed("");
@@ -274,7 +318,7 @@ void Download::downloadFinished()
}
m_reply.reset();
- qDebug() << "Download succeeded:" << m_url.toString();
+ qCDebug(taskDownloadLogC) << getUid().toString() << "Download succeeded:" << m_url.toString();
emit succeeded();
}
@@ -284,11 +328,11 @@ void Download::downloadReadyRead()
auto data = m_reply->readAll();
m_state = m_sink->write(data);
if (m_state == State::Failed) {
- qCritical() << "Failed to process response chunk";
+ qCCritical(taskDownloadLogC) << getUid().toString() << "Failed to process response chunk";
}
// qDebug() << "Download" << m_url.toString() << "gained" << data.size() << "bytes";
} else {
- qCritical() << "Cannot write download data! illegal status " << m_status;
+ qCCritical(taskDownloadLogC) << getUid().toString() << "Cannot write download data! illegal status " << m_status;
}
}
diff --git a/launcher/net/Download.h b/launcher/net/Download.h
index 7e1df322..2e861732 100644
--- a/launcher/net/Download.h
+++ b/launcher/net/Download.h
@@ -1,8 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
- * PolyMC - Minecraft Launcher
+ * Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.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
@@ -22,6 +23,7 @@
* 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
*
@@ -36,6 +38,8 @@
#pragma once
+#include <chrono>
+
#include "HttpMetaCache.h"
#include "NetAction.h"
#include "Sink.h"
@@ -56,7 +60,7 @@ class Download : public NetAction {
~Download() override = default;
static auto makeCached(QUrl url, MetaEntryPtr entry, Options options = Option::NoOptions) -> Download::Ptr;
- static auto makeByteArray(QUrl url, QByteArray* output, Options options = Option::NoOptions) -> Download::Ptr;
+ static auto makeByteArray(QUrl url, std::shared_ptr<QByteArray> output, Options options = Option::NoOptions) -> Download::Ptr;
static auto makeFile(QUrl url, QString path, Options options = Option::NoOptions) -> Download::Ptr;
public:
@@ -70,7 +74,7 @@ class Download : public NetAction {
protected slots:
void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) override;
void downloadError(QNetworkReply::NetworkError error) override;
- void sslErrors(const QList<QSslError>& errors);
+ void sslErrors(const QList<QSslError>& errors) override;
void downloadFinished() override;
void downloadReadyRead() override;
@@ -80,6 +84,10 @@ class Download : public NetAction {
private:
std::unique_ptr<Sink> m_sink;
Options m_options;
+
+ std::chrono::steady_clock m_clock;
+ std::chrono::time_point<std::chrono::steady_clock> m_last_progress_time;
+ qint64 m_last_progress_bytes;
};
} // namespace Net
diff --git a/launcher/net/FileSink.cpp b/launcher/net/FileSink.cpp
index ba0caf6c..1ecb21fd 100644
--- a/launcher/net/FileSink.cpp
+++ b/launcher/net/FileSink.cpp
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
- * PolyMC - Minecraft Launcher
+ * Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
@@ -37,6 +37,8 @@
#include "FileSystem.h"
+#include "net/Logging.h"
+
namespace Net {
Task::State FileSink::init(QNetworkRequest& request)
@@ -48,14 +50,14 @@ Task::State FileSink::init(QNetworkRequest& request)
// create a new save file and open it for writing
if (!FS::ensureFilePathExists(m_filename)) {
- qCritical() << "Could not create folder for " + m_filename;
+ qCCritical(taskNetLogC) << "Could not create folder for " + m_filename;
return Task::State::Failed;
}
wroteAnyData = false;
m_output_file.reset(new QSaveFile(m_filename));
if (!m_output_file->open(QIODevice::WriteOnly)) {
- qCritical() << "Could not open " + m_filename + " for writing";
+ qCCritical(taskNetLogC) << "Could not open " + m_filename + " for writing";
return Task::State::Failed;
}
@@ -67,7 +69,7 @@ Task::State FileSink::init(QNetworkRequest& request)
Task::State FileSink::write(QByteArray& data)
{
if (!writeAllValidators(data) || m_output_file->write(data) != data.size()) {
- qCritical() << "Failed writing into " + m_filename;
+ qCCritical(taskNetLogC) << "Failed writing into " + m_filename;
m_output_file->cancelWriting();
m_output_file.reset();
wroteAnyData = false;
@@ -106,7 +108,7 @@ Task::State FileSink::finalize(QNetworkReply& reply)
// nothing went wrong...
if (!m_output_file->commit()) {
- qCritical() << "Failed to commit changes to " << m_filename;
+ qCCritical(taskNetLogC) << "Failed to commit changes to " << m_filename;
m_output_file->cancelWriting();
return Task::State::Failed;
}
diff --git a/launcher/net/FileSink.h b/launcher/net/FileSink.h
index dffbdca6..40134b5f 100644
--- a/launcher/net/FileSink.h
+++ b/launcher/net/FileSink.h
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
- * PolyMC - Minecraft Launcher
+ * Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
diff --git a/launcher/net/HttpMetaCache.cpp b/launcher/net/HttpMetaCache.cpp
index 0d7ca769..689dbac9 100644
--- a/launcher/net/HttpMetaCache.cpp
+++ b/launcher/net/HttpMetaCache.cpp
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
- * PolyMC - Minecraft Launcher
+ * Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
@@ -44,6 +44,8 @@
#include <QDebug>
+#include "net/Logging.h"
+
auto MetaEntry::getFullPath() -> QString
{
// FIXME: make local?
@@ -55,7 +57,7 @@ HttpMetaCache::HttpMetaCache(QString path) : QObject(), m_index_file(path)
saveBatchingTimer.setSingleShot(true);
saveBatchingTimer.setTimerType(Qt::VeryCoarseTimer);
- connect(&saveBatchingTimer, SIGNAL(timeout()), SLOT(SaveNow()));
+ connect(&saveBatchingTimer, &QTimer::timeout, this, &HttpMetaCache::SaveNow);
}
HttpMetaCache::~HttpMetaCache()
@@ -124,7 +126,7 @@ auto HttpMetaCache::resolveEntry(QString base, QString resource_path, QString ex
// Get rid of old entries, to prevent cache problems
auto current_time = QDateTime::currentSecsSinceEpoch();
if (entry->isExpired(current_time - ( file_last_changed / 1000 ))) {
- qWarning() << "Removing cache entry because of old age!";
+ qCWarning(taskNetLogC) << "[HttpMetaCache]" << "Removing cache entry because of old age!";
selected_base.entry_list.remove(resource_path);
return staleEntry(base, resource_path);
}
@@ -137,12 +139,12 @@ auto HttpMetaCache::resolveEntry(QString base, QString resource_path, QString ex
auto HttpMetaCache::updateEntry(MetaEntryPtr stale_entry) -> bool
{
if (!m_entries.contains(stale_entry->m_baseId)) {
- qCritical() << "Cannot add entry with unknown base: " << stale_entry->m_baseId.toLocal8Bit();
+ qCCritical(taskHttpMetaCacheLogC) << "Cannot add entry with unknown base: " << stale_entry->m_baseId.toLocal8Bit();
return false;
}
if (stale_entry->m_stale) {
- qCritical() << "Cannot add stale entry: " << stale_entry->getFullPath().toLocal8Bit();
+ qCCritical(taskHttpMetaCacheLogC) << "Cannot add stale entry: " << stale_entry->getFullPath().toLocal8Bit();
return false;
}
@@ -166,10 +168,10 @@ void HttpMetaCache::evictAll()
{
for (QString& base : m_entries.keys()) {
EntryMap& map = m_entries[base];
- qDebug() << "Evicting base" << base;
+ qCDebug(taskHttpMetaCacheLogC) << "Evicting base" << base;
for (MetaEntryPtr entry : map.entry_list) {
if (!evictEntry(entry))
- qWarning() << "Unexpected missing cache entry" << entry->m_basePath;
+ qCWarning(taskHttpMetaCacheLogC) << "Unexpected missing cache entry" << entry->m_basePath;
}
}
}
@@ -267,7 +269,7 @@ void HttpMetaCache::SaveNow()
if (m_index_file.isNull())
return;
- qDebug() << "[HttpMetaCache]" << "Saving metacache with" << m_entries.size() << "entries";
+ qCDebug(taskHttpMetaCacheLogC) << "Saving metacache with" << m_entries.size() << "entries";
QJsonObject toplevel;
Json::writeString(toplevel, "version", "1");
@@ -302,6 +304,6 @@ void HttpMetaCache::SaveNow()
try {
Json::write(toplevel, m_index_file);
} catch (const Exception& e) {
- qWarning() << e.what();
+ qCWarning(taskHttpMetaCacheLogC) << "Error writing cache:" << e.what();
}
}
diff --git a/launcher/net/HttpMetaCache.h b/launcher/net/HttpMetaCache.h
index 37f4b49a..0dcb5668 100644
--- a/launcher/net/HttpMetaCache.h
+++ b/launcher/net/HttpMetaCache.h
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
- * PolyMC - Minecraft Launcher
+ * Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
diff --git a/launcher/net/Logging.cpp b/launcher/net/Logging.cpp
new file mode 100644
index 00000000..a9b9db7c
--- /dev/null
+++ b/launcher/net/Logging.cpp
@@ -0,0 +1,26 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "net/Logging.h"
+
+Q_LOGGING_CATEGORY(taskNetLogC, "launcher.task.net")
+Q_LOGGING_CATEGORY(taskDownloadLogC, "launcher.task.net.download")
+Q_LOGGING_CATEGORY(taskUploadLogC, "launcher.task.net.upload")
+Q_LOGGING_CATEGORY(taskMetaCacheLogC, "launcher.task.net.metacache")
+Q_LOGGING_CATEGORY(taskHttpMetaCacheLogC, "launcher.task.net.metacache.http")
diff --git a/launcher/net/Logging.h b/launcher/net/Logging.h
new file mode 100644
index 00000000..b692e707
--- /dev/null
+++ b/launcher/net/Logging.h
@@ -0,0 +1,28 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.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 <QLoggingCategory>
+
+Q_DECLARE_LOGGING_CATEGORY(taskNetLogC)
+Q_DECLARE_LOGGING_CATEGORY(taskDownloadLogC)
+Q_DECLARE_LOGGING_CATEGORY(taskUploadLogC)
+Q_DECLARE_LOGGING_CATEGORY(taskMetaCacheLogC)
+Q_DECLARE_LOGGING_CATEGORY(taskHttpMetaCacheLogC)
diff --git a/launcher/net/MetaCacheSink.cpp b/launcher/net/MetaCacheSink.cpp
index c730fdbf..e203bc06 100644
--- a/launcher/net/MetaCacheSink.cpp
+++ b/launcher/net/MetaCacheSink.cpp
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
- * PolyMC - Minecraft Launcher
+ * Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
@@ -39,6 +39,8 @@
#include <QRegularExpression>
#include "Application.h"
+#include "net/Logging.h"
+
namespace Net {
/** Maximum time to hold a cache entry
@@ -97,11 +99,11 @@ Task::State MetaCacheSink::finalizeCache(QNetworkReply & reply)
{ // Cache lifetime
if (m_is_eternal) {
- qDebug() << "[MetaCache] Adding eternal cache entry:" << m_entry->getFullPath();
+ qCDebug(taskMetaCacheLogC) << "Adding eternal cache entry:" << m_entry->getFullPath();
m_entry->makeEternal(true);
} else if (reply.hasRawHeader("Cache-Control")) {
auto cache_control_header = reply.rawHeader("Cache-Control");
- // qDebug() << "[MetaCache] Parsing 'Cache-Control' header with" << cache_control_header;
+ qCDebug(taskMetaCacheLogC) << "Parsing 'Cache-Control' header with" << cache_control_header;
QRegularExpression max_age_expr("max-age=([0-9]+)");
qint64 max_age = max_age_expr.match(cache_control_header).captured(1).toLongLong();
@@ -109,7 +111,7 @@ Task::State MetaCacheSink::finalizeCache(QNetworkReply & reply)
} else if (reply.hasRawHeader("Expires")) {
auto expires_header = reply.rawHeader("Expires");
- // qDebug() << "[MetaCache] Parsing 'Expires' header with" << expires_header;
+ qCDebug(taskMetaCacheLogC) << "Parsing 'Expires' header with" << expires_header;
qint64 max_age = QDateTime::fromString(expires_header).toSecsSinceEpoch() - QDateTime::currentSecsSinceEpoch();
m_entry->setMaximumAge(max_age);
@@ -119,7 +121,7 @@ Task::State MetaCacheSink::finalizeCache(QNetworkReply & reply)
if (reply.hasRawHeader("Age")) {
auto age_header = reply.rawHeader("Age");
- // qDebug() << "[MetaCache] Parsing 'Age' header with" << age_header;
+ qCDebug(taskMetaCacheLogC) << "Parsing 'Age' header with" << age_header;
qint64 current_age = age_header.toLongLong();
m_entry->setCurrentAge(current_age);
diff --git a/launcher/net/MetaCacheSink.h b/launcher/net/MetaCacheSink.h
index f5948085..f9f7d233 100644
--- a/launcher/net/MetaCacheSink.h
+++ b/launcher/net/MetaCacheSink.h
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
- * PolyMC - Minecraft Launcher
+ * Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
diff --git a/launcher/net/NetAction.h b/launcher/net/NetAction.h
index 38fe058b..ab9322c2 100644
--- a/launcher/net/NetAction.h
+++ b/launcher/net/NetAction.h
@@ -1,7 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
- * PolyMC - Minecraft Launcher
+ * Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
+ * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.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
@@ -44,7 +45,7 @@
class NetAction : public Task {
Q_OBJECT
protected:
- explicit NetAction() : Task() {};
+ explicit NetAction() : Task(){};
public:
using Ptr = shared_qobject_ptr<NetAction>;
@@ -61,6 +62,17 @@ class NetAction : public Task {
virtual void downloadFinished() = 0;
virtual void downloadReadyRead() = 0;
+ virtual void sslErrors(const QList<QSslError>& errors) {
+ int i = 1;
+ for (auto error : errors) {
+ qCritical() << "Network SSL Error #" << i << " : " << error.errorString();
+ auto cert = error.certificate();
+ qCritical() << "Certificate in question:\n" << cert.toText();
+ i++;
+ }
+
+ };
+
public slots:
void startAction(shared_qobject_ptr<QNetworkAccessManager> network)
{
@@ -69,7 +81,7 @@ class NetAction : public Task {
}
protected:
- void executeTask() override {};
+ void executeTask() override{};
public:
shared_qobject_ptr<QNetworkAccessManager> m_network;
diff --git a/launcher/net/NetJob.cpp b/launcher/net/NetJob.cpp
index 4bcd40b5..3869316e 100644
--- a/launcher/net/NetJob.cpp
+++ b/launcher/net/NetJob.cpp
@@ -1,8 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
- * PolyMC - Minecraft Launcher
+ * Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.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
diff --git a/launcher/net/NetJob.h b/launcher/net/NetJob.h
index cd5d5e48..764cec18 100644
--- a/launcher/net/NetJob.h
+++ b/launcher/net/NetJob.h
@@ -1,7 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
- * PolyMC - Minecraft Launcher
+ * Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
+ * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.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
diff --git a/launcher/net/PasteUpload.cpp b/launcher/net/PasteUpload.cpp
index d9e785c5..595279a3 100644
--- a/launcher/net/PasteUpload.cpp
+++ b/launcher/net/PasteUpload.cpp
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
- * PolyMC - Minecraft Launcher
+ * Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Lenny McLennington <lenny@sneed.church>
* Copyright (C) 2022 Swirl <swurl@swurl.xyz>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
@@ -47,6 +47,8 @@
#include <QFile>
#include <QUrlQuery>
+#include "net/Logging.h"
+
std::array<PasteUpload::PasteTypeInfo, 4> PasteUpload::PasteTypes = {
{{"0x0.st", "https://0x0.st", ""},
{"hastebin", "https://hst.sh", "/documents"},
@@ -147,7 +149,7 @@ void PasteUpload::executeTask()
void PasteUpload::downloadError(QNetworkReply::NetworkError error)
{
// error happened during download.
- qCritical() << "Network error: " << error;
+ qCCritical(taskUploadLogC) << getUid().toString() << "Network error: " << error;
emitFailed(m_reply->errorString());
}
@@ -166,7 +168,7 @@ void PasteUpload::downloadFinished()
{
QString reasonPhrase = m_reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString();
emitFailed(tr("Error: %1 returned unexpected status code %2 %3").arg(m_uploadUrl).arg(statusCode).arg(reasonPhrase));
- qCritical() << m_uploadUrl << " returned unexpected status code " << statusCode << " with body: " << data;
+ qCCritical(taskUploadLogC) << getUid().toString() << m_uploadUrl << " returned unexpected status code " << statusCode << " with body: " << data;
m_reply.reset();
return;
}
@@ -187,7 +189,7 @@ void PasteUpload::downloadFinished()
else
{
emitFailed(tr("Error: %1 returned a malformed response body").arg(m_uploadUrl));
- qCritical() << m_uploadUrl << " returned malformed response body: " << data;
+ qCCritical(taskUploadLogC) << getUid().toString() << getUid().toString() << m_uploadUrl << " returned malformed response body: " << data;
return;
}
break;
@@ -206,15 +208,15 @@ void PasteUpload::downloadFinished()
{
QString error = jsonObj["error"].toString();
emitFailed(tr("Error: %1 returned an error: %2").arg(m_uploadUrl, error));
- qCritical() << m_uploadUrl << " returned error: " << error;
- qCritical() << "Response body: " << data;
+ qCCritical(taskUploadLogC) << getUid().toString() << m_uploadUrl << " returned error: " << error;
+ qCCritical(taskUploadLogC) << getUid().toString() << "Response body: " << data;
return;
}
}
else
{
emitFailed(tr("Error: %1 returned a malformed response body").arg(m_uploadUrl));
- qCritical() << m_uploadUrl << " returned malformed response body: " << data;
+ qCCritical(taskUploadLogC) << getUid().toString() << m_uploadUrl << " returned malformed response body: " << data;
return;
}
break;
@@ -234,16 +236,16 @@ void PasteUpload::downloadFinished()
QString error = jsonObj["error"].toString();
QString message = (jsonObj.contains("message") && jsonObj["message"].isString()) ? jsonObj["message"].toString() : "none";
emitFailed(tr("Error: %1 returned an error code: %2\nError message: %3").arg(m_uploadUrl, error, message));
- qCritical() << m_uploadUrl << " returned error: " << error;
- qCritical() << "Error message: " << message;
- qCritical() << "Response body: " << data;
+ qCCritical(taskUploadLogC) << getUid().toString() << m_uploadUrl << " returned error: " << error;
+ qCCritical(taskUploadLogC) << getUid().toString() << "Error message: " << message;
+ qCCritical(taskUploadLogC) << getUid().toString() << "Response body: " << data;
return;
}
}
else
{
emitFailed(tr("Error: %1 returned a malformed response body").arg(m_uploadUrl));
- qCritical() << m_uploadUrl << " returned malformed response body: " << data;
+ qCCritical(taskUploadLogC) << getUid().toString() << m_uploadUrl << " returned malformed response body: " << data;
return;
}
break;
diff --git a/launcher/net/PasteUpload.h b/launcher/net/PasteUpload.h
index eb315c2b..b72ab5b0 100644
--- a/launcher/net/PasteUpload.h
+++ b/launcher/net/PasteUpload.h
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
- * PolyMC - Minecraft Launcher
+ * Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Lenny McLennington <lenny@sneed.church>
*
* This program is free software: you can redistribute it and/or modify
diff --git a/launcher/net/Upload.cpp b/launcher/net/Upload.cpp
index ccf43c2d..3f6f5829 100644
--- a/launcher/net/Upload.cpp
+++ b/launcher/net/Upload.cpp
@@ -4,6 +4,7 @@
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
+ * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.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
@@ -38,212 +39,226 @@
#include "Upload.h"
#include <utility>
-#include "ByteArraySink.h"
-#include "BuildConfig.h"
#include "Application.h"
+#include "BuildConfig.h"
+#include "ByteArraySink.h"
+
+#include "net/Logging.h"
namespace Net {
- bool Upload::abort()
- {
- if (m_reply) {
- m_reply->abort();
- } else {
- m_state = State::AbortedByUser;
- }
- return true;
+bool Upload::abort()
+{
+ if (m_reply) {
+ m_reply->abort();
+ } else {
+ m_state = State::AbortedByUser;
}
+ return true;
+}
- void Upload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) {
- setProgress(bytesReceived, bytesTotal);
- }
+void Upload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal)
+{
+ setProgress(bytesReceived, bytesTotal);
+}
- void Upload::downloadError(QNetworkReply::NetworkError error) {
- if (error == QNetworkReply::OperationCanceledError) {
- qCritical() << "Aborted " << m_url.toString();
- m_state = State::AbortedByUser;
- } else {
- // error happened during download.
- qCritical() << "Failed " << m_url.toString() << " with reason " << error;
- m_state = State::Failed;
- }
+void Upload::downloadError(QNetworkReply::NetworkError error)
+{
+ if (error == QNetworkReply::OperationCanceledError) {
+ qCCritical(taskUploadLogC) << getUid().toString() << "Aborted " << m_url.toString();
+ m_state = State::AbortedByUser;
+ } else {
+ // error happened during download.
+ qCCritical(taskUploadLogC) << getUid().toString() << "Failed " << m_url.toString() << " with reason " << error;
+ m_state = State::Failed;
}
+}
- void Upload::sslErrors(const QList<QSslError> &errors) {
- int i = 1;
- for (const auto& error : errors) {
- qCritical() << "Upload" << m_url.toString() << "SSL Error #" << i << " : " << error.errorString();
- auto cert = error.certificate();
- qCritical() << "Certificate in question:\n" << cert.toText();
- i++;
- }
+void Upload::sslErrors(const QList<QSslError>& errors)
+{
+ int i = 1;
+ for (const auto& error : errors) {
+ qCCritical(taskUploadLogC) << getUid().toString() << "Upload" << m_url.toString() << "SSL Error #" << i << " : "
+ << error.errorString();
+ auto cert = error.certificate();
+ qCCritical(taskUploadLogC) << getUid().toString() << "Certificate in question:\n" << cert.toText();
+ i++;
}
+}
- bool Upload::handleRedirect()
- {
- QUrl redirect = m_reply->header(QNetworkRequest::LocationHeader).toUrl();
- if (!redirect.isValid()) {
- if (!m_reply->hasRawHeader("Location")) {
- // no redirect -> it's fine to continue
- return false;
- }
- // there is a Location header, but it's not correct. we need to apply some workarounds...
- QByteArray redirectBA = m_reply->rawHeader("Location");
- if (redirectBA.size() == 0) {
- // empty, yet present redirect header? WTF?
- return false;
- }
- QString redirectStr = QString::fromUtf8(redirectBA);
-
- if (redirectStr.startsWith("//")) {
- /*
- * IF the URL begins with //, we need to insert the URL scheme.
- * See: https://bugreports.qt.io/browse/QTBUG-41061
- * See: http://tools.ietf.org/html/rfc3986#section-4.2
- */
- redirectStr = m_reply->url().scheme() + ":" + redirectStr;
- } else if (redirectStr.startsWith("/")) {
- /*
- * IF the URL begins with /, we need to process it as a relative URL
- */
- auto url = m_reply->url();
- url.setPath(redirectStr, QUrl::TolerantMode);
- redirectStr = url.toString();
- }
+bool Upload::handleRedirect()
+{
+ QUrl redirect = m_reply->header(QNetworkRequest::LocationHeader).toUrl();
+ if (!redirect.isValid()) {
+ if (!m_reply->hasRawHeader("Location")) {
+ // no redirect -> it's fine to continue
+ return false;
+ }
+ // there is a Location header, but it's not correct. we need to apply some workarounds...
+ QByteArray redirectBA = m_reply->rawHeader("Location");
+ if (redirectBA.size() == 0) {
+ // empty, yet present redirect header? WTF?
+ return false;
+ }
+ QString redirectStr = QString::fromUtf8(redirectBA);
+ if (redirectStr.startsWith("//")) {
/*
- * Next, make sure the URL is parsed in tolerant mode. Qt doesn't parse the location header in tolerant mode, which causes issues.
- * FIXME: report Qt bug for this
+ * IF the URL begins with //, we need to insert the URL scheme.
+ * See: https://bugreports.qt.io/browse/QTBUG-41061
+ * See: http://tools.ietf.org/html/rfc3986#section-4.2
*/
- redirect = QUrl(redirectStr, QUrl::TolerantMode);
- if (!redirect.isValid()) {
- qWarning() << "Failed to parse redirect URL:" << redirectStr;
- downloadError(QNetworkReply::ProtocolFailure);
- return false;
- }
- qDebug() << "Fixed location header:" << redirect;
- } else {
- qDebug() << "Location header:" << redirect;
+ redirectStr = m_reply->url().scheme() + ":" + redirectStr;
+ } else if (redirectStr.startsWith("/")) {
+ /*
+ * IF the URL begins with /, we need to process it as a relative URL
+ */
+ auto url = m_reply->url();
+ url.setPath(redirectStr, QUrl::TolerantMode);
+ redirectStr = url.toString();
}
- m_url = QUrl(redirect.toString());
- qDebug() << "Following redirect to " << m_url.toString();
- startAction(m_network);
- return true;
- }
-
- void Upload::downloadFinished() {
- // handle HTTP redirection first
- // very unlikely for post requests, still can happen
- if (handleRedirect()) {
- qDebug() << "Upload redirected:" << m_url.toString();
- return;
+ /*
+ * Next, make sure the URL is parsed in tolerant mode. Qt doesn't parse the location header in tolerant mode, which causes issues.
+ * FIXME: report Qt bug for this
+ */
+ redirect = QUrl(redirectStr, QUrl::TolerantMode);
+ if (!redirect.isValid()) {
+ qCWarning(taskUploadLogC) << getUid().toString() << "Failed to parse redirect URL:" << redirectStr;
+ downloadError(QNetworkReply::ProtocolFailure);
+ return false;
}
+ qCDebug(taskUploadLogC) << getUid().toString() << "Fixed location header:" << redirect;
+ } else {
+ qCDebug(taskUploadLogC) << getUid().toString() << "Location header:" << redirect;
+ }
- // if the download failed before this point ...
- if (m_state == State::Succeeded) {
- qDebug() << "Upload failed but we are allowed to proceed:" << m_url.toString();
- m_sink->abort();
- m_reply.reset();
- emit succeeded();
- return;
- } else if (m_state == State::Failed) {
- qDebug() << "Upload failed in previous step:" << m_url.toString();
- m_sink->abort();
- m_reply.reset();
- emit failed("");
- return;
- } else if (m_state == State::AbortedByUser) {
- qDebug() << "Upload aborted in previous step:" << m_url.toString();
- m_sink->abort();
- m_reply.reset();
- emit aborted();
- return;
- }
+ m_url = QUrl(redirect.toString());
+ qCDebug(taskUploadLogC) << getUid().toString() << "Following redirect to " << m_url.toString();
+ startAction(m_network);
+ return true;
+}
- // make sure we got all the remaining data, if any
- auto data = m_reply->readAll();
- if (data.size()) {
- qDebug() << "Writing extra" << data.size() << "bytes";
- m_state = m_sink->write(data);
- }
+void Upload::downloadFinished()
+{
+ // handle HTTP redirection first
+ // very unlikely for post requests, still can happen
+ if (handleRedirect()) {
+ qCDebug(taskUploadLogC) << getUid().toString() << "Upload redirected:" << m_url.toString();
+ return;
+ }
- // otherwise, finalize the whole graph
- m_state = m_sink->finalize(*m_reply.get());
- if (m_state != State::Succeeded) {
- qDebug() << "Upload failed to finalize:" << m_url.toString();
- m_sink->abort();
- m_reply.reset();
- emit failed("");
- return;
- }
+ // if the download failed before this point ...
+ if (m_state == State::Succeeded) {
+ qCDebug(taskUploadLogC) << getUid().toString() << "Upload failed but we are allowed to proceed:" << m_url.toString();
+ m_sink->abort();
m_reply.reset();
- qDebug() << "Upload succeeded:" << m_url.toString();
emit succeeded();
+ return;
+ } else if (m_state == State::Failed) {
+ qCDebug(taskUploadLogC) << getUid().toString() << "Upload failed in previous step:" << m_url.toString();
+ m_sink->abort();
+ m_reply.reset();
+ emit failed("");
+ return;
+ } else if (m_state == State::AbortedByUser) {
+ qCDebug(taskUploadLogC) << getUid().toString() << "Upload aborted in previous step:" << m_url.toString();
+ m_sink->abort();
+ m_reply.reset();
+ emit aborted();
+ return;
}
- void Upload::downloadReadyRead() {
- if (m_state == State::Running) {
- auto data = m_reply->readAll();
- m_state = m_sink->write(data);
- }
+ // make sure we got all the remaining data, if any
+ auto data = m_reply->readAll();
+ if (data.size()) {
+ qCDebug(taskUploadLogC) << getUid().toString() << "Writing extra" << data.size() << "bytes";
+ m_state = m_sink->write(data);
}
- void Upload::executeTask() {
- setStatus(tr("Uploading %1").arg(m_url.toString()));
-
- if (m_state == State::AbortedByUser) {
- qWarning() << "Attempt to start an aborted Upload:" << m_url.toString();
- emit aborted();
- return;
- }
- QNetworkRequest request(m_url);
- m_state = m_sink->init(request);
- switch (m_state) {
- case State::Succeeded:
- emitSucceeded();
- qDebug() << "Upload cache hit " << m_url.toString();
- return;
- case State::Running:
- qDebug() << "Uploading " << m_url.toString();
- break;
- case State::Inactive:
- case State::Failed:
- emitFailed("");
- return;
- case State::AbortedByUser:
- emitAborted();
- return;
- }
+ // otherwise, finalize the whole graph
+ m_state = m_sink->finalize(*m_reply.get());
+ if (m_state != State::Succeeded) {
+ qCDebug(taskUploadLogC) << getUid().toString() << "Upload failed to finalize:" << m_url.toString();
+ m_sink->abort();
+ m_reply.reset();
+ emit failed("");
+ return;
+ }
+ m_reply.reset();
+ qCDebug(taskUploadLogC) << getUid().toString() << "Upload succeeded:" << m_url.toString();
+ emit succeeded();
+}
- request.setHeader(QNetworkRequest::UserAgentHeader, APPLICATION->getUserAgent().toUtf8());
- // TODO remove duplication
- if (APPLICATION->capabilities() & Application::SupportsFlame && request.url().host() == QUrl(BuildConfig.FLAME_BASE_URL).host()) {
- request.setRawHeader("x-api-key", APPLICATION->getFlameAPIKey().toUtf8());
- } else if (request.url().host() == QUrl(BuildConfig.MODRINTH_PROD_URL).host() ||
- request.url().host() == QUrl(BuildConfig.MODRINTH_STAGING_URL).host()) {
- QString token = APPLICATION->getModrinthAPIToken();
- if (!token.isNull())
- request.setRawHeader("Authorization", token.toUtf8());
- }
+void Upload::downloadReadyRead()
+{
+ if (m_state == State::Running) {
+ auto data = m_reply->readAll();
+ m_state = m_sink->write(data);
+ }
+}
- //TODO other types of post requests ?
- request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
- QNetworkReply* rep = m_network->post(request, m_post_data);
+void Upload::executeTask()
+{
+ setStatus(tr("Uploading %1").arg(m_url.toString()));
- m_reply.reset(rep);
- connect(rep, SIGNAL(downloadProgress(qint64, qint64)), SLOT(downloadProgress(qint64, qint64)));
- connect(rep, SIGNAL(finished()), SLOT(downloadFinished()));
- connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), SLOT(downloadError(QNetworkReply::NetworkError)));
- connect(rep, &QNetworkReply::sslErrors, this, &Upload::sslErrors);
- connect(rep, &QNetworkReply::readyRead, this, &Upload::downloadReadyRead);
+ if (m_state == State::AbortedByUser) {
+ qCWarning(taskUploadLogC) << getUid().toString() << "Attempt to start an aborted Upload:" << m_url.toString();
+ emit aborted();
+ return;
+ }
+ QNetworkRequest request(m_url);
+ m_state = m_sink->init(request);
+ switch (m_state) {
+ case State::Succeeded:
+ emitSucceeded();
+ qCDebug(taskUploadLogC) << getUid().toString() << "Upload cache hit " << m_url.toString();
+ return;
+ case State::Running:
+ qCDebug(taskUploadLogC) << getUid().toString() << "Uploading " << m_url.toString();
+ break;
+ case State::Inactive:
+ case State::Failed:
+ emitFailed("");
+ return;
+ case State::AbortedByUser:
+ emitAborted();
+ return;
}
- Upload::Ptr Upload::makeByteArray(QUrl url, QByteArray *output, QByteArray m_post_data) {
- 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);
- return up;
+ request.setHeader(QNetworkRequest::UserAgentHeader, APPLICATION->getUserAgent().toUtf8());
+ // TODO remove duplication
+ if (APPLICATION->capabilities() & Application::SupportsFlame && request.url().host() == QUrl(BuildConfig.FLAME_BASE_URL).host()) {
+ request.setRawHeader("x-api-key", APPLICATION->getFlameAPIKey().toUtf8());
+ } else if (request.url().host() == QUrl(BuildConfig.MODRINTH_PROD_URL).host() ||
+ request.url().host() == QUrl(BuildConfig.MODRINTH_STAGING_URL).host()) {
+ QString token = APPLICATION->getModrinthAPIToken();
+ if (!token.isNull())
+ request.setRawHeader("Authorization", token.toUtf8());
}
-} // Net
+
+ // TODO other types of post requests ?
+ request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
+ QNetworkReply* rep = m_network->post(request, m_post_data);
+
+ m_reply.reset(rep);
+ connect(rep, &QNetworkReply::downloadProgress, this, &Upload::downloadProgress);
+ connect(rep, &QNetworkReply::finished, this, &Upload::downloadFinished);
+#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15
+ connect(rep, &QNetworkReply::errorOccurred, this, &Upload::downloadError);
+#else
+ connect(rep, QOverload<QNetworkReply::NetworkError>::of(&QNetworkReply::error), this, &Upload::downloadError);
+#endif
+ connect(rep, &QNetworkReply::sslErrors, this, &Upload::sslErrors);
+ connect(rep, &QNetworkReply::readyRead, this, &Upload::downloadReadyRead);
+}
+
+Upload::Ptr Upload::makeByteArray(QUrl url, std::shared_ptr<QByteArray> output, QByteArray m_post_data)
+{
+ 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);
+ return up;
+}
+} // namespace Net
diff --git a/launcher/net/Upload.h b/launcher/net/Upload.h
index 5a0b2e74..0b0c9497 100644
--- a/launcher/net/Upload.h
+++ b/launcher/net/Upload.h
@@ -1,8 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
- * PolyMC - Minecraft Launcher
+ * Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.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
@@ -41,31 +42,31 @@
namespace Net {
- class Upload : public NetAction {
- Q_OBJECT
+class Upload : public NetAction {
+ Q_OBJECT
- public:
- using Ptr = shared_qobject_ptr<Upload>;
+ 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; };
+ static Upload::Ptr makeByteArray(QUrl url, std::shared_ptr<QByteArray> output, QByteArray m_post_data);
+ auto abort() -> bool override;
+ auto canAbort() const -> bool override { return true; };
- protected slots:
- void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) override;
- void downloadError(QNetworkReply::NetworkError error) override;
- void sslErrors(const QList<QSslError> & errors);
- void downloadFinished() override;
- void downloadReadyRead() override;
+ protected slots:
+ void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) override;
+ void downloadError(QNetworkReply::NetworkError error) override;
+ void sslErrors(const QList<QSslError>& errors) override;
+ void downloadFinished() override;
+ void downloadReadyRead() override;
- public slots:
- void executeTask() override;
- private:
- std::unique_ptr<Sink> m_sink;
- QByteArray m_post_data;
+ public slots:
+ void executeTask() override;
- bool handleRedirect();
- };
+ private:
+ std::unique_ptr<Sink> m_sink;
+ QByteArray m_post_data;
-} // Net
+ bool handleRedirect();
+};
+} // namespace Net
diff --git a/launcher/news/NewsChecker.cpp b/launcher/news/NewsChecker.cpp
index 1f1520d0..4f02bf5e 100644
--- a/launcher/news/NewsChecker.cpp
+++ b/launcher/news/NewsChecker.cpp
@@ -58,7 +58,7 @@ void NewsChecker::reloadNews()
qDebug() << "Reloading news.";
NetJob::Ptr job{ new NetJob("News RSS Feed", m_network) };
- job->addNetAction(Net::Download::makeByteArray(m_feedUrl, &newsData));
+ job->addNetAction(Net::Download::makeByteArray(m_feedUrl, newsData));
QObject::connect(job.get(), &NetJob::succeeded, this, &NewsChecker::rssDownloadFinished);
QObject::connect(job.get(), &NetJob::failed, this, &NewsChecker::rssDownloadFailed);
m_newsNetJob.reset(job);
@@ -79,32 +79,27 @@ void NewsChecker::rssDownloadFinished()
int errorCol = -1;
// Parse the XML.
- if (!doc.setContent(newsData, false, &errorMsg, &errorLine, &errorCol))
- {
+ if (!doc.setContent(*newsData, false, &errorMsg, &errorLine, &errorCol)) {
QString fullErrorMsg = QString("Error parsing RSS feed XML. %1 at %2:%3.").arg(errorMsg).arg(errorLine).arg(errorCol);
fail(fullErrorMsg);
- newsData.clear();
+ newsData->clear();
return;
}
- newsData.clear();
+ newsData->clear();
}
// If the parsing succeeded, read it.
QDomNodeList items = doc.elementsByTagName("entry");
m_newsEntries.clear();
- for (int i = 0; i < items.length(); i++)
- {
+ for (int i = 0; i < items.length(); i++) {
QDomElement element = items.at(i).toElement();
NewsEntryPtr entry;
entry.reset(new NewsEntry());
QString errorMsg = "An unknown error occurred.";
- if (NewsEntry::fromXmlElement(element, entry.get(), &errorMsg))
- {
+ if (NewsEntry::fromXmlElement(element, entry.get(), &errorMsg)) {
qDebug() << "Loaded news entry" << entry->title;
m_newsEntries.append(entry);
- }
- else
- {
+ } else {
qWarning() << "Failed to load news entry at index" << i << ":" << errorMsg;
}
}
diff --git a/launcher/news/NewsChecker.h b/launcher/news/NewsChecker.h
index 8467a541..41babfff 100644
--- a/launcher/news/NewsChecker.h
+++ b/launcher/news/NewsChecker.h
@@ -85,7 +85,7 @@ protected: /* data */
//! True if news has been loaded.
bool m_loadedNews;
- QByteArray newsData;
+ std::shared_ptr<QByteArray> newsData = std::make_shared<QByteArray>();
/*!
* Gets the error message that was given last time the news was loaded.
diff --git a/launcher/qtlogging.ini b/launcher/qtlogging.ini
new file mode 100644
index 00000000..c12d1e10
--- /dev/null
+++ b/launcher/qtlogging.ini
@@ -0,0 +1,16 @@
+[Rules]
+*.debug=true
+# prevent log spam and strange bugs
+# qt.qpa.drawing in particular causes theme artifacts on MacOS
+qt.*.debug=false
+# don't log credentials by default
+launcher.auth.credentials.debug=false
+# remove the debug lines, other log levels still get through
+launcher.task.net.download.debug=false
+# enable or disable whole catageries
+launcher.task.net=true
+launcher.task=false
+launcher.task.net.upload=true
+launcher.task.net.metacache=false
+launcher.task.net.metacache.http=true
+
diff --git a/launcher/resources/OSX/OSX.qrc b/launcher/resources/OSX/OSX.qrc
index 9d4511d1..49f56b0c 100644
--- a/launcher/resources/OSX/OSX.qrc
+++ b/launcher/resources/OSX/OSX.qrc
@@ -16,7 +16,6 @@
<file>scalable/jarmods.svg</file>
<file>scalable/java.svg</file>
<file>scalable/language.svg</file>
- <file>scalable/launcher.svg</file>
<file>scalable/loadermods.svg</file>
<file>scalable/log.svg</file>
<file>scalable/minecraft.svg</file>
diff --git a/launcher/resources/OSX/scalable/launcher.svg b/launcher/resources/OSX/scalable/launcher.svg
deleted file mode 100644
index aeee8433..00000000
--- a/launcher/resources/OSX/scalable/launcher.svg
+++ /dev/null
@@ -1,57 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-<svg width="48" height="48" version="1.1" viewBox="0 0 12.7 12.7" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
- <title>Prism Launcher Logo</title>
- <g stroke-width=".26458">
- <path d="m6.35 6.35" fill="#99cd61"/>
- <path d="m6.35 0.52917-2.5208 4.3656 2.5208 1.4552 2.5203-1.4552 0.10955-3.0996c-1.1511-0.66459-2.3388-1.2661-2.6298-1.2661z" fill="#df6277"/>
- <path d="m8.9798 1.7952-2.6298 4.5548 2.5203 1.4552 2.5208-4.3656c-0.14552-0.25205-1.2601-0.97975-2.4112-1.6443z" fill="#fb9168"/>
- <path d="m11.391 3.4396-5.041 2.9104 2.5203 1.4552 2.7389-1.4552c0-1.3292-0.072554-2.6584-0.21808-2.9104z" fill="#f3db6c"/>
- <path d="m6.35 6.35v2.9104h5.041c0.14552-0.25205 0.21807-1.5812 0.21808-2.9104h-5.2591z" fill="#7ab392"/>
- <path d="m6.35 6.35v2.9104l2.6298 1.6443c1.1511-0.66459 2.2657-1.3923 2.4112-1.6443l-5.041-2.9104z" fill="#4b7cbc"/>
- <path d="m6.35 6.35-2.5208 1.4552 2.5208 4.3656c0.29104 0 1.4787-0.60148 2.6298-1.2661l-2.6298-4.5548z" fill="#6f488c"/>
- <path d="m3.8292 4.8948-2.5203 4.3656c0.29104 0.5041 4.459 2.9104 5.041 2.9104v-5.8208l-2.5208-1.4552z" fill="#4d3f33"/>
- <path d="m1.309 3.4396c-0.29104 0.5041-0.29104 5.3167 0 5.8208l5.041-2.9104v-2.9104h-5.041z" fill="#7a573b"/>
- <path d="m6.35 0.52917c-0.58208-2e-8 -4.75 2.4063-5.041 2.9104l5.041 2.9104v-5.8208z" fill="#99cd61"/>
- </g>
- <g transform="matrix(.88 0 0 .88 -10.906 -1.2421)">
- <g transform="translate(13.26 2.2776)">
- <path transform="matrix(.96975 0 0 .96975 .1921 .1921)" d="m6.3498 2.9393c-0.34105 0-2.7827 1.4099-2.9532 1.7052l2.9532 5.1157 2.9538-5.1157c-0.17052-0.29535-2.6127-1.7052-2.9538-1.7052z" fill="#fff" stroke-width=".26458"/>
- </g>
- <path d="m16.746 6.9737 2.8639 4.9609c0.33073 0 2.6991-1.3672 2.8644-1.6536 0.16536-0.28642 0.16536-3.0209 0-3.3073l-2.8644 1.6536z" fill="#dfdfdf" stroke-width=".26458"/>
- </g>
- <path d="m3.8299 4.8948c-0.14551 0.25205-0.14553 2.6584 0 2.9104 0.14553 0.25204 2.2292 1.4552 2.5203 1.4552v-2.9104z" fill="#d6d2d2" stroke-width=".26458"/>
- <metadata>
- <rdf:RDF>
- <cc:Work rdf:about="">
- <dc:title>Prism Launcher Logo</dc:title>
- <dc:date>19/10/2022</dc:date>
- <dc:creator>
- <cc:Agent>
- <dc:title>Prism Launcher</dc:title>
- </cc:Agent>
- </dc:creator>
- <dc:contributor>
- <cc:Agent>
- <dc:title>AutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zeke</dc:title>
- </cc:Agent>
- </dc:contributor>
- <dc:source>https://github.com/PrismLauncher/PrismLauncher</dc:source>
- <dc:publisher>
- <cc:Agent>
- <dc:title>Prism Launcher</dc:title>
- </cc:Agent>
- </dc:publisher>
- <cc:license rdf:resource="http://creativecommons.org/licenses/by-sa/4.0/"/>
- </cc:Work>
- <cc:License rdf:about="http://creativecommons.org/licenses/by-sa/4.0/">
- <cc:permits rdf:resource="http://creativecommons.org/ns#Reproduction"/>
- <cc:permits rdf:resource="http://creativecommons.org/ns#Distribution"/>
- <cc:requires rdf:resource="http://creativecommons.org/ns#Notice"/>
- <cc:requires rdf:resource="http://creativecommons.org/ns#Attribution"/>
- <cc:permits rdf:resource="http://creativecommons.org/ns#DerivativeWorks"/>
- <cc:requires rdf:resource="http://creativecommons.org/ns#ShareAlike"/>
- </cc:License>
- </rdf:RDF>
- </metadata>
-</svg>
diff --git a/launcher/resources/breeze_dark/scalable/launcher.svg b/launcher/resources/breeze_dark/scalable/launcher.svg
deleted file mode 100644
index aeee8433..00000000
--- a/launcher/resources/breeze_dark/scalable/launcher.svg
+++ /dev/null
@@ -1,57 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-<svg width="48" height="48" version="1.1" viewBox="0 0 12.7 12.7" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
- <title>Prism Launcher Logo</title>
- <g stroke-width=".26458">
- <path d="m6.35 6.35" fill="#99cd61"/>
- <path d="m6.35 0.52917-2.5208 4.3656 2.5208 1.4552 2.5203-1.4552 0.10955-3.0996c-1.1511-0.66459-2.3388-1.2661-2.6298-1.2661z" fill="#df6277"/>
- <path d="m8.9798 1.7952-2.6298 4.5548 2.5203 1.4552 2.5208-4.3656c-0.14552-0.25205-1.2601-0.97975-2.4112-1.6443z" fill="#fb9168"/>
- <path d="m11.391 3.4396-5.041 2.9104 2.5203 1.4552 2.7389-1.4552c0-1.3292-0.072554-2.6584-0.21808-2.9104z" fill="#f3db6c"/>
- <path d="m6.35 6.35v2.9104h5.041c0.14552-0.25205 0.21807-1.5812 0.21808-2.9104h-5.2591z" fill="#7ab392"/>
- <path d="m6.35 6.35v2.9104l2.6298 1.6443c1.1511-0.66459 2.2657-1.3923 2.4112-1.6443l-5.041-2.9104z" fill="#4b7cbc"/>
- <path d="m6.35 6.35-2.5208 1.4552 2.5208 4.3656c0.29104 0 1.4787-0.60148 2.6298-1.2661l-2.6298-4.5548z" fill="#6f488c"/>
- <path d="m3.8292 4.8948-2.5203 4.3656c0.29104 0.5041 4.459 2.9104 5.041 2.9104v-5.8208l-2.5208-1.4552z" fill="#4d3f33"/>
- <path d="m1.309 3.4396c-0.29104 0.5041-0.29104 5.3167 0 5.8208l5.041-2.9104v-2.9104h-5.041z" fill="#7a573b"/>
- <path d="m6.35 0.52917c-0.58208-2e-8 -4.75 2.4063-5.041 2.9104l5.041 2.9104v-5.8208z" fill="#99cd61"/>
- </g>
- <g transform="matrix(.88 0 0 .88 -10.906 -1.2421)">
- <g transform="translate(13.26 2.2776)">
- <path transform="matrix(.96975 0 0 .96975 .1921 .1921)" d="m6.3498 2.9393c-0.34105 0-2.7827 1.4099-2.9532 1.7052l2.9532 5.1157 2.9538-5.1157c-0.17052-0.29535-2.6127-1.7052-2.9538-1.7052z" fill="#fff" stroke-width=".26458"/>
- </g>
- <path d="m16.746 6.9737 2.8639 4.9609c0.33073 0 2.6991-1.3672 2.8644-1.6536 0.16536-0.28642 0.16536-3.0209 0-3.3073l-2.8644 1.6536z" fill="#dfdfdf" stroke-width=".26458"/>
- </g>
- <path d="m3.8299 4.8948c-0.14551 0.25205-0.14553 2.6584 0 2.9104 0.14553 0.25204 2.2292 1.4552 2.5203 1.4552v-2.9104z" fill="#d6d2d2" stroke-width=".26458"/>
- <metadata>
- <rdf:RDF>
- <cc:Work rdf:about="">
- <dc:title>Prism Launcher Logo</dc:title>
- <dc:date>19/10/2022</dc:date>
- <dc:creator>
- <cc:Agent>
- <dc:title>Prism Launcher</dc:title>
- </cc:Agent>
- </dc:creator>
- <dc:contributor>
- <cc:Agent>
- <dc:title>AutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zeke</dc:title>
- </cc:Agent>
- </dc:contributor>
- <dc:source>https://github.com/PrismLauncher/PrismLauncher</dc:source>
- <dc:publisher>
- <cc:Agent>
- <dc:title>Prism Launcher</dc:title>
- </cc:Agent>
- </dc:publisher>
- <cc:license rdf:resource="http://creativecommons.org/licenses/by-sa/4.0/"/>
- </cc:Work>
- <cc:License rdf:about="http://creativecommons.org/licenses/by-sa/4.0/">
- <cc:permits rdf:resource="http://creativecommons.org/ns#Reproduction"/>
- <cc:permits rdf:resource="http://creativecommons.org/ns#Distribution"/>
- <cc:requires rdf:resource="http://creativecommons.org/ns#Notice"/>
- <cc:requires rdf:resource="http://creativecommons.org/ns#Attribution"/>
- <cc:permits rdf:resource="http://creativecommons.org/ns#DerivativeWorks"/>
- <cc:requires rdf:resource="http://creativecommons.org/ns#ShareAlike"/>
- </cc:License>
- </rdf:RDF>
- </metadata>
-</svg>
diff --git a/launcher/resources/flat/flat.qrc b/launcher/resources/flat/flat.qrc
index cadf8736..2fd5daef 100644
--- a/launcher/resources/flat/flat.qrc
+++ b/launcher/resources/flat/flat.qrc
@@ -18,7 +18,6 @@
<file>scalable/jarmods.svg</file>
<file>scalable/java.svg</file>
<file>scalable/language.svg</file>
- <file>scalable/launcher.svg</file>
<file>scalable/loadermods.svg</file>
<file>scalable/log.svg</file>
<file>scalable/minecraft.svg</file>
diff --git a/launcher/resources/flat/scalable/launcher.svg b/launcher/resources/flat/scalable/launcher.svg
deleted file mode 100644
index aeee8433..00000000
--- a/launcher/resources/flat/scalable/launcher.svg
+++ /dev/null
@@ -1,57 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-<svg width="48" height="48" version="1.1" viewBox="0 0 12.7 12.7" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
- <title>Prism Launcher Logo</title>
- <g stroke-width=".26458">
- <path d="m6.35 6.35" fill="#99cd61"/>
- <path d="m6.35 0.52917-2.5208 4.3656 2.5208 1.4552 2.5203-1.4552 0.10955-3.0996c-1.1511-0.66459-2.3388-1.2661-2.6298-1.2661z" fill="#df6277"/>
- <path d="m8.9798 1.7952-2.6298 4.5548 2.5203 1.4552 2.5208-4.3656c-0.14552-0.25205-1.2601-0.97975-2.4112-1.6443z" fill="#fb9168"/>
- <path d="m11.391 3.4396-5.041 2.9104 2.5203 1.4552 2.7389-1.4552c0-1.3292-0.072554-2.6584-0.21808-2.9104z" fill="#f3db6c"/>
- <path d="m6.35 6.35v2.9104h5.041c0.14552-0.25205 0.21807-1.5812 0.21808-2.9104h-5.2591z" fill="#7ab392"/>
- <path d="m6.35 6.35v2.9104l2.6298 1.6443c1.1511-0.66459 2.2657-1.3923 2.4112-1.6443l-5.041-2.9104z" fill="#4b7cbc"/>
- <path d="m6.35 6.35-2.5208 1.4552 2.5208 4.3656c0.29104 0 1.4787-0.60148 2.6298-1.2661l-2.6298-4.5548z" fill="#6f488c"/>
- <path d="m3.8292 4.8948-2.5203 4.3656c0.29104 0.5041 4.459 2.9104 5.041 2.9104v-5.8208l-2.5208-1.4552z" fill="#4d3f33"/>
- <path d="m1.309 3.4396c-0.29104 0.5041-0.29104 5.3167 0 5.8208l5.041-2.9104v-2.9104h-5.041z" fill="#7a573b"/>
- <path d="m6.35 0.52917c-0.58208-2e-8 -4.75 2.4063-5.041 2.9104l5.041 2.9104v-5.8208z" fill="#99cd61"/>
- </g>
- <g transform="matrix(.88 0 0 .88 -10.906 -1.2421)">
- <g transform="translate(13.26 2.2776)">
- <path transform="matrix(.96975 0 0 .96975 .1921 .1921)" d="m6.3498 2.9393c-0.34105 0-2.7827 1.4099-2.9532 1.7052l2.9532 5.1157 2.9538-5.1157c-0.17052-0.29535-2.6127-1.7052-2.9538-1.7052z" fill="#fff" stroke-width=".26458"/>
- </g>
- <path d="m16.746 6.9737 2.8639 4.9609c0.33073 0 2.6991-1.3672 2.8644-1.6536 0.16536-0.28642 0.16536-3.0209 0-3.3073l-2.8644 1.6536z" fill="#dfdfdf" stroke-width=".26458"/>
- </g>
- <path d="m3.8299 4.8948c-0.14551 0.25205-0.14553 2.6584 0 2.9104 0.14553 0.25204 2.2292 1.4552 2.5203 1.4552v-2.9104z" fill="#d6d2d2" stroke-width=".26458"/>
- <metadata>
- <rdf:RDF>
- <cc:Work rdf:about="">
- <dc:title>Prism Launcher Logo</dc:title>
- <dc:date>19/10/2022</dc:date>
- <dc:creator>
- <cc:Agent>
- <dc:title>Prism Launcher</dc:title>
- </cc:Agent>
- </dc:creator>
- <dc:contributor>
- <cc:Agent>
- <dc:title>AutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zeke</dc:title>
- </cc:Agent>
- </dc:contributor>
- <dc:source>https://github.com/PrismLauncher/PrismLauncher</dc:source>
- <dc:publisher>
- <cc:Agent>
- <dc:title>Prism Launcher</dc:title>
- </cc:Agent>
- </dc:publisher>
- <cc:license rdf:resource="http://creativecommons.org/licenses/by-sa/4.0/"/>
- </cc:Work>
- <cc:License rdf:about="http://creativecommons.org/licenses/by-sa/4.0/">
- <cc:permits rdf:resource="http://creativecommons.org/ns#Reproduction"/>
- <cc:permits rdf:resource="http://creativecommons.org/ns#Distribution"/>
- <cc:requires rdf:resource="http://creativecommons.org/ns#Notice"/>
- <cc:requires rdf:resource="http://creativecommons.org/ns#Attribution"/>
- <cc:permits rdf:resource="http://creativecommons.org/ns#DerivativeWorks"/>
- <cc:requires rdf:resource="http://creativecommons.org/ns#ShareAlike"/>
- </cc:License>
- </rdf:RDF>
- </metadata>
-</svg>
diff --git a/launcher/resources/flat_white/flat_white.qrc b/launcher/resources/flat_white/flat_white.qrc
index 2701462f..a1c940da 100644
--- a/launcher/resources/flat_white/flat_white.qrc
+++ b/launcher/resources/flat_white/flat_white.qrc
@@ -18,7 +18,6 @@
<file>scalable/jarmods.svg</file>
<file>scalable/java.svg</file>
<file>scalable/language.svg</file>
- <file>scalable/launcher.svg</file>
<file>scalable/loadermods.svg</file>
<file>scalable/log.svg</file>
<file>scalable/minecraft.svg</file>
diff --git a/launcher/resources/flat_white/scalable/launcher.svg b/launcher/resources/flat_white/scalable/launcher.svg
deleted file mode 100644
index 54131b65..00000000
--- a/launcher/resources/flat_white/scalable/launcher.svg
+++ /dev/null
@@ -1,2 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<svg width="24" height="24" fill="#eeeeee" version="1.1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m20 4h-16v16h16zm0 18h-16c-1.1046 0-2-0.89543-2-2v-16c0-1.1046 0.89543-2 2-2h16c1.1046 0 2 0.89543 2 2v16c0 1.1046-0.89543 2-2 2z"/><path d="m7.2 18c-0.225 0-0.45-0.075-0.6-0.15-0.375-0.225-0.6-0.6-0.6-1.05v-9.6c0-0.45 0.225-0.825 0.6-1.05 0.225-0.15 0.375-0.15 0.6-0.15 0.15 0 0.375 0.075 0.525 0.15l9.6 4.8c0.375 0.225 0.675 0.6 0.675 1.05 0 0.45-0.225 0.9-0.675 1.05l-9.6 4.8c-0.15 0.075-0.375 0.15-0.525 0.15z" clip-rule="evenodd" fill="#eeeeee" fill-rule="evenodd" stroke-width=".99999"/></svg>
diff --git a/launcher/resources/iOS/iOS.qrc b/launcher/resources/iOS/iOS.qrc
index 0b79efb2..9b8d84f5 100644
--- a/launcher/resources/iOS/iOS.qrc
+++ b/launcher/resources/iOS/iOS.qrc
@@ -16,7 +16,6 @@
<file>scalable/jarmods.svg</file>
<file>scalable/java.svg</file>
<file>scalable/language.svg</file>
- <file>scalable/launcher.svg</file>
<file>scalable/loadermods.svg</file>
<file>scalable/log.svg</file>
<file>scalable/minecraft.svg</file>
diff --git a/launcher/resources/iOS/scalable/launcher.svg b/launcher/resources/iOS/scalable/launcher.svg
deleted file mode 100644
index aeee8433..00000000
--- a/launcher/resources/iOS/scalable/launcher.svg
+++ /dev/null
@@ -1,57 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-<svg width="48" height="48" version="1.1" viewBox="0 0 12.7 12.7" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
- <title>Prism Launcher Logo</title>
- <g stroke-width=".26458">
- <path d="m6.35 6.35" fill="#99cd61"/>
- <path d="m6.35 0.52917-2.5208 4.3656 2.5208 1.4552 2.5203-1.4552 0.10955-3.0996c-1.1511-0.66459-2.3388-1.2661-2.6298-1.2661z" fill="#df6277"/>
- <path d="m8.9798 1.7952-2.6298 4.5548 2.5203 1.4552 2.5208-4.3656c-0.14552-0.25205-1.2601-0.97975-2.4112-1.6443z" fill="#fb9168"/>
- <path d="m11.391 3.4396-5.041 2.9104 2.5203 1.4552 2.7389-1.4552c0-1.3292-0.072554-2.6584-0.21808-2.9104z" fill="#f3db6c"/>
- <path d="m6.35 6.35v2.9104h5.041c0.14552-0.25205 0.21807-1.5812 0.21808-2.9104h-5.2591z" fill="#7ab392"/>
- <path d="m6.35 6.35v2.9104l2.6298 1.6443c1.1511-0.66459 2.2657-1.3923 2.4112-1.6443l-5.041-2.9104z" fill="#4b7cbc"/>
- <path d="m6.35 6.35-2.5208 1.4552 2.5208 4.3656c0.29104 0 1.4787-0.60148 2.6298-1.2661l-2.6298-4.5548z" fill="#6f488c"/>
- <path d="m3.8292 4.8948-2.5203 4.3656c0.29104 0.5041 4.459 2.9104 5.041 2.9104v-5.8208l-2.5208-1.4552z" fill="#4d3f33"/>
- <path d="m1.309 3.4396c-0.29104 0.5041-0.29104 5.3167 0 5.8208l5.041-2.9104v-2.9104h-5.041z" fill="#7a573b"/>
- <path d="m6.35 0.52917c-0.58208-2e-8 -4.75 2.4063-5.041 2.9104l5.041 2.9104v-5.8208z" fill="#99cd61"/>
- </g>
- <g transform="matrix(.88 0 0 .88 -10.906 -1.2421)">
- <g transform="translate(13.26 2.2776)">
- <path transform="matrix(.96975 0 0 .96975 .1921 .1921)" d="m6.3498 2.9393c-0.34105 0-2.7827 1.4099-2.9532 1.7052l2.9532 5.1157 2.9538-5.1157c-0.17052-0.29535-2.6127-1.7052-2.9538-1.7052z" fill="#fff" stroke-width=".26458"/>
- </g>
- <path d="m16.746 6.9737 2.8639 4.9609c0.33073 0 2.6991-1.3672 2.8644-1.6536 0.16536-0.28642 0.16536-3.0209 0-3.3073l-2.8644 1.6536z" fill="#dfdfdf" stroke-width=".26458"/>
- </g>
- <path d="m3.8299 4.8948c-0.14551 0.25205-0.14553 2.6584 0 2.9104 0.14553 0.25204 2.2292 1.4552 2.5203 1.4552v-2.9104z" fill="#d6d2d2" stroke-width=".26458"/>
- <metadata>
- <rdf:RDF>
- <cc:Work rdf:about="">
- <dc:title>Prism Launcher Logo</dc:title>
- <dc:date>19/10/2022</dc:date>
- <dc:creator>
- <cc:Agent>
- <dc:title>Prism Launcher</dc:title>
- </cc:Agent>
- </dc:creator>
- <dc:contributor>
- <cc:Agent>
- <dc:title>AutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zeke</dc:title>
- </cc:Agent>
- </dc:contributor>
- <dc:source>https://github.com/PrismLauncher/PrismLauncher</dc:source>
- <dc:publisher>
- <cc:Agent>
- <dc:title>Prism Launcher</dc:title>
- </cc:Agent>
- </dc:publisher>
- <cc:license rdf:resource="http://creativecommons.org/licenses/by-sa/4.0/"/>
- </cc:Work>
- <cc:License rdf:about="http://creativecommons.org/licenses/by-sa/4.0/">
- <cc:permits rdf:resource="http://creativecommons.org/ns#Reproduction"/>
- <cc:permits rdf:resource="http://creativecommons.org/ns#Distribution"/>
- <cc:requires rdf:resource="http://creativecommons.org/ns#Notice"/>
- <cc:requires rdf:resource="http://creativecommons.org/ns#Attribution"/>
- <cc:permits rdf:resource="http://creativecommons.org/ns#DerivativeWorks"/>
- <cc:requires rdf:resource="http://creativecommons.org/ns#ShareAlike"/>
- </cc:License>
- </rdf:RDF>
- </metadata>
-</svg>
diff --git a/launcher/resources/pe_blue/pe_blue.qrc b/launcher/resources/pe_blue/pe_blue.qrc
index 1b2b6291..da45ef9a 100644
--- a/launcher/resources/pe_blue/pe_blue.qrc
+++ b/launcher/resources/pe_blue/pe_blue.qrc
@@ -16,7 +16,6 @@
<file>scalable/jarmods.svg</file>
<file>scalable/java.svg</file>
<file>scalable/language.svg</file>
- <file>scalable/launcher.svg</file>
<file>scalable/loadermods.svg</file>
<file>scalable/log.svg</file>
<file>scalable/minecraft.svg</file>
diff --git a/launcher/resources/pe_blue/scalable/launcher.svg b/launcher/resources/pe_blue/scalable/launcher.svg
deleted file mode 100644
index aeee8433..00000000
--- a/launcher/resources/pe_blue/scalable/launcher.svg
+++ /dev/null
@@ -1,57 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-<svg width="48" height="48" version="1.1" viewBox="0 0 12.7 12.7" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
- <title>Prism Launcher Logo</title>
- <g stroke-width=".26458">
- <path d="m6.35 6.35" fill="#99cd61"/>
- <path d="m6.35 0.52917-2.5208 4.3656 2.5208 1.4552 2.5203-1.4552 0.10955-3.0996c-1.1511-0.66459-2.3388-1.2661-2.6298-1.2661z" fill="#df6277"/>
- <path d="m8.9798 1.7952-2.6298 4.5548 2.5203 1.4552 2.5208-4.3656c-0.14552-0.25205-1.2601-0.97975-2.4112-1.6443z" fill="#fb9168"/>
- <path d="m11.391 3.4396-5.041 2.9104 2.5203 1.4552 2.7389-1.4552c0-1.3292-0.072554-2.6584-0.21808-2.9104z" fill="#f3db6c"/>
- <path d="m6.35 6.35v2.9104h5.041c0.14552-0.25205 0.21807-1.5812 0.21808-2.9104h-5.2591z" fill="#7ab392"/>
- <path d="m6.35 6.35v2.9104l2.6298 1.6443c1.1511-0.66459 2.2657-1.3923 2.4112-1.6443l-5.041-2.9104z" fill="#4b7cbc"/>
- <path d="m6.35 6.35-2.5208 1.4552 2.5208 4.3656c0.29104 0 1.4787-0.60148 2.6298-1.2661l-2.6298-4.5548z" fill="#6f488c"/>
- <path d="m3.8292 4.8948-2.5203 4.3656c0.29104 0.5041 4.459 2.9104 5.041 2.9104v-5.8208l-2.5208-1.4552z" fill="#4d3f33"/>
- <path d="m1.309 3.4396c-0.29104 0.5041-0.29104 5.3167 0 5.8208l5.041-2.9104v-2.9104h-5.041z" fill="#7a573b"/>
- <path d="m6.35 0.52917c-0.58208-2e-8 -4.75 2.4063-5.041 2.9104l5.041 2.9104v-5.8208z" fill="#99cd61"/>
- </g>
- <g transform="matrix(.88 0 0 .88 -10.906 -1.2421)">
- <g transform="translate(13.26 2.2776)">
- <path transform="matrix(.96975 0 0 .96975 .1921 .1921)" d="m6.3498 2.9393c-0.34105 0-2.7827 1.4099-2.9532 1.7052l2.9532 5.1157 2.9538-5.1157c-0.17052-0.29535-2.6127-1.7052-2.9538-1.7052z" fill="#fff" stroke-width=".26458"/>
- </g>
- <path d="m16.746 6.9737 2.8639 4.9609c0.33073 0 2.6991-1.3672 2.8644-1.6536 0.16536-0.28642 0.16536-3.0209 0-3.3073l-2.8644 1.6536z" fill="#dfdfdf" stroke-width=".26458"/>
- </g>
- <path d="m3.8299 4.8948c-0.14551 0.25205-0.14553 2.6584 0 2.9104 0.14553 0.25204 2.2292 1.4552 2.5203 1.4552v-2.9104z" fill="#d6d2d2" stroke-width=".26458"/>
- <metadata>
- <rdf:RDF>
- <cc:Work rdf:about="">
- <dc:title>Prism Launcher Logo</dc:title>
- <dc:date>19/10/2022</dc:date>
- <dc:creator>
- <cc:Agent>
- <dc:title>Prism Launcher</dc:title>
- </cc:Agent>
- </dc:creator>
- <dc:contributor>
- <cc:Agent>
- <dc:title>AutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zeke</dc:title>
- </cc:Agent>
- </dc:contributor>
- <dc:source>https://github.com/PrismLauncher/PrismLauncher</dc:source>
- <dc:publisher>
- <cc:Agent>
- <dc:title>Prism Launcher</dc:title>
- </cc:Agent>
- </dc:publisher>
- <cc:license rdf:resource="http://creativecommons.org/licenses/by-sa/4.0/"/>
- </cc:Work>
- <cc:License rdf:about="http://creativecommons.org/licenses/by-sa/4.0/">
- <cc:permits rdf:resource="http://creativecommons.org/ns#Reproduction"/>
- <cc:permits rdf:resource="http://creativecommons.org/ns#Distribution"/>
- <cc:requires rdf:resource="http://creativecommons.org/ns#Notice"/>
- <cc:requires rdf:resource="http://creativecommons.org/ns#Attribution"/>
- <cc:permits rdf:resource="http://creativecommons.org/ns#DerivativeWorks"/>
- <cc:requires rdf:resource="http://creativecommons.org/ns#ShareAlike"/>
- </cc:License>
- </rdf:RDF>
- </metadata>
-</svg>
diff --git a/launcher/resources/pe_colored/pe_colored.qrc b/launcher/resources/pe_colored/pe_colored.qrc
index 084fca93..ba5bd44f 100644
--- a/launcher/resources/pe_colored/pe_colored.qrc
+++ b/launcher/resources/pe_colored/pe_colored.qrc
@@ -16,7 +16,6 @@
<file>scalable/jarmods.svg</file>
<file>scalable/java.svg</file>
<file>scalable/language.svg</file>
- <file>scalable/launcher.svg</file>
<file>scalable/loadermods.svg</file>
<file>scalable/log.svg</file>
<file>scalable/minecraft.svg</file>
diff --git a/launcher/resources/pe_colored/scalable/launcher.svg b/launcher/resources/pe_colored/scalable/launcher.svg
deleted file mode 100644
index aeee8433..00000000
--- a/launcher/resources/pe_colored/scalable/launcher.svg
+++ /dev/null
@@ -1,57 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-<svg width="48" height="48" version="1.1" viewBox="0 0 12.7 12.7" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
- <title>Prism Launcher Logo</title>
- <g stroke-width=".26458">
- <path d="m6.35 6.35" fill="#99cd61"/>
- <path d="m6.35 0.52917-2.5208 4.3656 2.5208 1.4552 2.5203-1.4552 0.10955-3.0996c-1.1511-0.66459-2.3388-1.2661-2.6298-1.2661z" fill="#df6277"/>
- <path d="m8.9798 1.7952-2.6298 4.5548 2.5203 1.4552 2.5208-4.3656c-0.14552-0.25205-1.2601-0.97975-2.4112-1.6443z" fill="#fb9168"/>
- <path d="m11.391 3.4396-5.041 2.9104 2.5203 1.4552 2.7389-1.4552c0-1.3292-0.072554-2.6584-0.21808-2.9104z" fill="#f3db6c"/>
- <path d="m6.35 6.35v2.9104h5.041c0.14552-0.25205 0.21807-1.5812 0.21808-2.9104h-5.2591z" fill="#7ab392"/>
- <path d="m6.35 6.35v2.9104l2.6298 1.6443c1.1511-0.66459 2.2657-1.3923 2.4112-1.6443l-5.041-2.9104z" fill="#4b7cbc"/>
- <path d="m6.35 6.35-2.5208 1.4552 2.5208 4.3656c0.29104 0 1.4787-0.60148 2.6298-1.2661l-2.6298-4.5548z" fill="#6f488c"/>
- <path d="m3.8292 4.8948-2.5203 4.3656c0.29104 0.5041 4.459 2.9104 5.041 2.9104v-5.8208l-2.5208-1.4552z" fill="#4d3f33"/>
- <path d="m1.309 3.4396c-0.29104 0.5041-0.29104 5.3167 0 5.8208l5.041-2.9104v-2.9104h-5.041z" fill="#7a573b"/>
- <path d="m6.35 0.52917c-0.58208-2e-8 -4.75 2.4063-5.041 2.9104l5.041 2.9104v-5.8208z" fill="#99cd61"/>
- </g>
- <g transform="matrix(.88 0 0 .88 -10.906 -1.2421)">
- <g transform="translate(13.26 2.2776)">
- <path transform="matrix(.96975 0 0 .96975 .1921 .1921)" d="m6.3498 2.9393c-0.34105 0-2.7827 1.4099-2.9532 1.7052l2.9532 5.1157 2.9538-5.1157c-0.17052-0.29535-2.6127-1.7052-2.9538-1.7052z" fill="#fff" stroke-width=".26458"/>
- </g>
- <path d="m16.746 6.9737 2.8639 4.9609c0.33073 0 2.6991-1.3672 2.8644-1.6536 0.16536-0.28642 0.16536-3.0209 0-3.3073l-2.8644 1.6536z" fill="#dfdfdf" stroke-width=".26458"/>
- </g>
- <path d="m3.8299 4.8948c-0.14551 0.25205-0.14553 2.6584 0 2.9104 0.14553 0.25204 2.2292 1.4552 2.5203 1.4552v-2.9104z" fill="#d6d2d2" stroke-width=".26458"/>
- <metadata>
- <rdf:RDF>
- <cc:Work rdf:about="">
- <dc:title>Prism Launcher Logo</dc:title>
- <dc:date>19/10/2022</dc:date>
- <dc:creator>
- <cc:Agent>
- <dc:title>Prism Launcher</dc:title>
- </cc:Agent>
- </dc:creator>
- <dc:contributor>
- <cc:Agent>
- <dc:title>AutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zeke</dc:title>
- </cc:Agent>
- </dc:contributor>
- <dc:source>https://github.com/PrismLauncher/PrismLauncher</dc:source>
- <dc:publisher>
- <cc:Agent>
- <dc:title>Prism Launcher</dc:title>
- </cc:Agent>
- </dc:publisher>
- <cc:license rdf:resource="http://creativecommons.org/licenses/by-sa/4.0/"/>
- </cc:Work>
- <cc:License rdf:about="http://creativecommons.org/licenses/by-sa/4.0/">
- <cc:permits rdf:resource="http://creativecommons.org/ns#Reproduction"/>
- <cc:permits rdf:resource="http://creativecommons.org/ns#Distribution"/>
- <cc:requires rdf:resource="http://creativecommons.org/ns#Notice"/>
- <cc:requires rdf:resource="http://creativecommons.org/ns#Attribution"/>
- <cc:permits rdf:resource="http://creativecommons.org/ns#DerivativeWorks"/>
- <cc:requires rdf:resource="http://creativecommons.org/ns#ShareAlike"/>
- </cc:License>
- </rdf:RDF>
- </metadata>
-</svg>
diff --git a/launcher/resources/pe_dark/pe_dark.qrc b/launcher/resources/pe_dark/pe_dark.qrc
index 5c49b75a..2bfec42c 100644
--- a/launcher/resources/pe_dark/pe_dark.qrc
+++ b/launcher/resources/pe_dark/pe_dark.qrc
@@ -16,7 +16,6 @@
<file>scalable/jarmods.svg</file>
<file>scalable/java.svg</file>
<file>scalable/language.svg</file>
- <file>scalable/launcher.svg</file>
<file>scalable/loadermods.svg</file>
<file>scalable/log.svg</file>
<file>scalable/minecraft.svg</file>
diff --git a/launcher/resources/pe_dark/scalable/launcher.svg b/launcher/resources/pe_dark/scalable/launcher.svg
deleted file mode 100644
index aeee8433..00000000
--- a/launcher/resources/pe_dark/scalable/launcher.svg
+++ /dev/null
@@ -1,57 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-<svg width="48" height="48" version="1.1" viewBox="0 0 12.7 12.7" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
- <title>Prism Launcher Logo</title>
- <g stroke-width=".26458">
- <path d="m6.35 6.35" fill="#99cd61"/>
- <path d="m6.35 0.52917-2.5208 4.3656 2.5208 1.4552 2.5203-1.4552 0.10955-3.0996c-1.1511-0.66459-2.3388-1.2661-2.6298-1.2661z" fill="#df6277"/>
- <path d="m8.9798 1.7952-2.6298 4.5548 2.5203 1.4552 2.5208-4.3656c-0.14552-0.25205-1.2601-0.97975-2.4112-1.6443z" fill="#fb9168"/>
- <path d="m11.391 3.4396-5.041 2.9104 2.5203 1.4552 2.7389-1.4552c0-1.3292-0.072554-2.6584-0.21808-2.9104z" fill="#f3db6c"/>
- <path d="m6.35 6.35v2.9104h5.041c0.14552-0.25205 0.21807-1.5812 0.21808-2.9104h-5.2591z" fill="#7ab392"/>
- <path d="m6.35 6.35v2.9104l2.6298 1.6443c1.1511-0.66459 2.2657-1.3923 2.4112-1.6443l-5.041-2.9104z" fill="#4b7cbc"/>
- <path d="m6.35 6.35-2.5208 1.4552 2.5208 4.3656c0.29104 0 1.4787-0.60148 2.6298-1.2661l-2.6298-4.5548z" fill="#6f488c"/>
- <path d="m3.8292 4.8948-2.5203 4.3656c0.29104 0.5041 4.459 2.9104 5.041 2.9104v-5.8208l-2.5208-1.4552z" fill="#4d3f33"/>
- <path d="m1.309 3.4396c-0.29104 0.5041-0.29104 5.3167 0 5.8208l5.041-2.9104v-2.9104h-5.041z" fill="#7a573b"/>
- <path d="m6.35 0.52917c-0.58208-2e-8 -4.75 2.4063-5.041 2.9104l5.041 2.9104v-5.8208z" fill="#99cd61"/>
- </g>
- <g transform="matrix(.88 0 0 .88 -10.906 -1.2421)">
- <g transform="translate(13.26 2.2776)">
- <path transform="matrix(.96975 0 0 .96975 .1921 .1921)" d="m6.3498 2.9393c-0.34105 0-2.7827 1.4099-2.9532 1.7052l2.9532 5.1157 2.9538-5.1157c-0.17052-0.29535-2.6127-1.7052-2.9538-1.7052z" fill="#fff" stroke-width=".26458"/>
- </g>
- <path d="m16.746 6.9737 2.8639 4.9609c0.33073 0 2.6991-1.3672 2.8644-1.6536 0.16536-0.28642 0.16536-3.0209 0-3.3073l-2.8644 1.6536z" fill="#dfdfdf" stroke-width=".26458"/>
- </g>
- <path d="m3.8299 4.8948c-0.14551 0.25205-0.14553 2.6584 0 2.9104 0.14553 0.25204 2.2292 1.4552 2.5203 1.4552v-2.9104z" fill="#d6d2d2" stroke-width=".26458"/>
- <metadata>
- <rdf:RDF>
- <cc:Work rdf:about="">
- <dc:title>Prism Launcher Logo</dc:title>
- <dc:date>19/10/2022</dc:date>
- <dc:creator>
- <cc:Agent>
- <dc:title>Prism Launcher</dc:title>
- </cc:Agent>
- </dc:creator>
- <dc:contributor>
- <cc:Agent>
- <dc:title>AutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zeke</dc:title>
- </cc:Agent>
- </dc:contributor>
- <dc:source>https://github.com/PrismLauncher/PrismLauncher</dc:source>
- <dc:publisher>
- <cc:Agent>
- <dc:title>Prism Launcher</dc:title>
- </cc:Agent>
- </dc:publisher>
- <cc:license rdf:resource="http://creativecommons.org/licenses/by-sa/4.0/"/>
- </cc:Work>
- <cc:License rdf:about="http://creativecommons.org/licenses/by-sa/4.0/">
- <cc:permits rdf:resource="http://creativecommons.org/ns#Reproduction"/>
- <cc:permits rdf:resource="http://creativecommons.org/ns#Distribution"/>
- <cc:requires rdf:resource="http://creativecommons.org/ns#Notice"/>
- <cc:requires rdf:resource="http://creativecommons.org/ns#Attribution"/>
- <cc:permits rdf:resource="http://creativecommons.org/ns#DerivativeWorks"/>
- <cc:requires rdf:resource="http://creativecommons.org/ns#ShareAlike"/>
- </cc:License>
- </rdf:RDF>
- </metadata>
-</svg>
diff --git a/launcher/resources/pe_light/pe_light.qrc b/launcher/resources/pe_light/pe_light.qrc
index a8e3f157..25d5da73 100644
--- a/launcher/resources/pe_light/pe_light.qrc
+++ b/launcher/resources/pe_light/pe_light.qrc
@@ -16,7 +16,6 @@
<file>scalable/jarmods.svg</file>
<file>scalable/java.svg</file>
<file>scalable/language.svg</file>
- <file>scalable/launcher.svg</file>
<file>scalable/loadermods.svg</file>
<file>scalable/log.svg</file>
<file>scalable/minecraft.svg</file>
diff --git a/launcher/resources/pe_light/scalable/launcher.svg b/launcher/resources/pe_light/scalable/launcher.svg
deleted file mode 100644
index aeee8433..00000000
--- a/launcher/resources/pe_light/scalable/launcher.svg
+++ /dev/null
@@ -1,57 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-<svg width="48" height="48" version="1.1" viewBox="0 0 12.7 12.7" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
- <title>Prism Launcher Logo</title>
- <g stroke-width=".26458">
- <path d="m6.35 6.35" fill="#99cd61"/>
- <path d="m6.35 0.52917-2.5208 4.3656 2.5208 1.4552 2.5203-1.4552 0.10955-3.0996c-1.1511-0.66459-2.3388-1.2661-2.6298-1.2661z" fill="#df6277"/>
- <path d="m8.9798 1.7952-2.6298 4.5548 2.5203 1.4552 2.5208-4.3656c-0.14552-0.25205-1.2601-0.97975-2.4112-1.6443z" fill="#fb9168"/>
- <path d="m11.391 3.4396-5.041 2.9104 2.5203 1.4552 2.7389-1.4552c0-1.3292-0.072554-2.6584-0.21808-2.9104z" fill="#f3db6c"/>
- <path d="m6.35 6.35v2.9104h5.041c0.14552-0.25205 0.21807-1.5812 0.21808-2.9104h-5.2591z" fill="#7ab392"/>
- <path d="m6.35 6.35v2.9104l2.6298 1.6443c1.1511-0.66459 2.2657-1.3923 2.4112-1.6443l-5.041-2.9104z" fill="#4b7cbc"/>
- <path d="m6.35 6.35-2.5208 1.4552 2.5208 4.3656c0.29104 0 1.4787-0.60148 2.6298-1.2661l-2.6298-4.5548z" fill="#6f488c"/>
- <path d="m3.8292 4.8948-2.5203 4.3656c0.29104 0.5041 4.459 2.9104 5.041 2.9104v-5.8208l-2.5208-1.4552z" fill="#4d3f33"/>
- <path d="m1.309 3.4396c-0.29104 0.5041-0.29104 5.3167 0 5.8208l5.041-2.9104v-2.9104h-5.041z" fill="#7a573b"/>
- <path d="m6.35 0.52917c-0.58208-2e-8 -4.75 2.4063-5.041 2.9104l5.041 2.9104v-5.8208z" fill="#99cd61"/>
- </g>
- <g transform="matrix(.88 0 0 .88 -10.906 -1.2421)">
- <g transform="translate(13.26 2.2776)">
- <path transform="matrix(.96975 0 0 .96975 .1921 .1921)" d="m6.3498 2.9393c-0.34105 0-2.7827 1.4099-2.9532 1.7052l2.9532 5.1157 2.9538-5.1157c-0.17052-0.29535-2.6127-1.7052-2.9538-1.7052z" fill="#fff" stroke-width=".26458"/>
- </g>
- <path d="m16.746 6.9737 2.8639 4.9609c0.33073 0 2.6991-1.3672 2.8644-1.6536 0.16536-0.28642 0.16536-3.0209 0-3.3073l-2.8644 1.6536z" fill="#dfdfdf" stroke-width=".26458"/>
- </g>
- <path d="m3.8299 4.8948c-0.14551 0.25205-0.14553 2.6584 0 2.9104 0.14553 0.25204 2.2292 1.4552 2.5203 1.4552v-2.9104z" fill="#d6d2d2" stroke-width=".26458"/>
- <metadata>
- <rdf:RDF>
- <cc:Work rdf:about="">
- <dc:title>Prism Launcher Logo</dc:title>
- <dc:date>19/10/2022</dc:date>
- <dc:creator>
- <cc:Agent>
- <dc:title>Prism Launcher</dc:title>
- </cc:Agent>
- </dc:creator>
- <dc:contributor>
- <cc:Agent>
- <dc:title>AutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zeke</dc:title>
- </cc:Agent>
- </dc:contributor>
- <dc:source>https://github.com/PrismLauncher/PrismLauncher</dc:source>
- <dc:publisher>
- <cc:Agent>
- <dc:title>Prism Launcher</dc:title>
- </cc:Agent>
- </dc:publisher>
- <cc:license rdf:resource="http://creativecommons.org/licenses/by-sa/4.0/"/>
- </cc:Work>
- <cc:License rdf:about="http://creativecommons.org/licenses/by-sa/4.0/">
- <cc:permits rdf:resource="http://creativecommons.org/ns#Reproduction"/>
- <cc:permits rdf:resource="http://creativecommons.org/ns#Distribution"/>
- <cc:requires rdf:resource="http://creativecommons.org/ns#Notice"/>
- <cc:requires rdf:resource="http://creativecommons.org/ns#Attribution"/>
- <cc:permits rdf:resource="http://creativecommons.org/ns#DerivativeWorks"/>
- <cc:requires rdf:resource="http://creativecommons.org/ns#ShareAlike"/>
- </cc:License>
- </rdf:RDF>
- </metadata>
-</svg>
diff --git a/launcher/screenshots/ImgurAlbumCreation.cpp b/launcher/screenshots/ImgurAlbumCreation.cpp
index a72c32d3..ab425f1a 100644
--- a/launcher/screenshots/ImgurAlbumCreation.cpp
+++ b/launcher/screenshots/ImgurAlbumCreation.cpp
@@ -74,17 +74,20 @@ void ImgurAlbumCreation::executeTask()
m_reply.reset(rep);
connect(rep, &QNetworkReply::uploadProgress, this, &ImgurAlbumCreation::downloadProgress);
connect(rep, &QNetworkReply::finished, this, &ImgurAlbumCreation::downloadFinished);
-#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
- connect(rep, SIGNAL(errorOccurred(QNetworkReply::NetworkError)), SLOT(downloadError(QNetworkReply::NetworkError)));
+#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15
+ connect(rep, &QNetworkReply::errorOccurred, this, &ImgurAlbumCreation::downloadError);
#else
- connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), SLOT(downloadError(QNetworkReply::NetworkError)));
+ connect(rep, QOverload<QNetworkReply::NetworkError>::of(&QNetworkReply::error), this, &ImgurAlbumCreation::downloadError);
#endif
+ connect(rep, &QNetworkReply::sslErrors, this, &ImgurAlbumCreation::sslErrors);
}
+
void ImgurAlbumCreation::downloadError(QNetworkReply::NetworkError error)
{
qDebug() << m_reply->errorString();
m_state = State::Failed;
}
+
void ImgurAlbumCreation::downloadFinished()
{
if (m_state != State::Failed)
@@ -120,6 +123,7 @@ void ImgurAlbumCreation::downloadFinished()
return;
}
}
+
void ImgurAlbumCreation::downloadProgress(qint64 bytesReceived, qint64 bytesTotal)
{
setProgress(bytesReceived, bytesTotal);
diff --git a/launcher/screenshots/ImgurUpload.cpp b/launcher/screenshots/ImgurUpload.cpp
index f8ac9bc2..a50f9afa 100644
--- a/launcher/screenshots/ImgurUpload.cpp
+++ b/launcher/screenshots/ImgurUpload.cpp
@@ -89,12 +89,14 @@ void ImgurUpload::executeTask()
m_reply.reset(rep);
connect(rep, &QNetworkReply::uploadProgress, this, &ImgurUpload::downloadProgress);
connect(rep, &QNetworkReply::finished, this, &ImgurUpload::downloadFinished);
-#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
- connect(rep, SIGNAL(errorOccurred(QNetworkReply::NetworkError)), SLOT(downloadError(QNetworkReply::NetworkError)));
+#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15
+ connect(rep, &QNetworkReply::errorOccurred, this, &ImgurUpload::downloadError);
#else
- connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), SLOT(downloadError(QNetworkReply::NetworkError)));
+ connect(rep, QOverload<QNetworkReply::NetworkError>::of(&QNetworkReply::error), this, &ImgurUpload::downloadError);
#endif
+ connect(rep, &QNetworkReply::sslErrors, this, &ImgurUpload::sslErrors);
}
+
void ImgurUpload::downloadError(QNetworkReply::NetworkError error)
{
qCritical() << "ImgurUpload failed with error" << m_reply->errorString() << "Server reply:\n" << m_reply->readAll();
@@ -108,6 +110,7 @@ void ImgurUpload::downloadError(QNetworkReply::NetworkError error)
m_reply.reset();
emitFailed();
}
+
void ImgurUpload::downloadFinished()
{
if(finished)
@@ -144,6 +147,7 @@ void ImgurUpload::downloadFinished()
emit succeeded();
return;
}
+
void ImgurUpload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal)
{
setProgress(bytesReceived, bytesTotal);
diff --git a/launcher/settings/INIFile.cpp b/launcher/settings/INIFile.cpp
index 733cd444..d16256b9 100644
--- a/launcher/settings/INIFile.cpp
+++ b/launcher/settings/INIFile.cpp
@@ -1,7 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
- * PolyMC - Minecraft Launcher
+ * Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ * Copyright (C) 2023 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
@@ -42,32 +43,51 @@
#include <QSaveFile>
#include <QDebug>
-INIFile::INIFile()
+#include <QSettings>
+
+INIFile::INIFile() {}
+
+bool INIFile::saveFile(QString fileName)
{
-}
+ if (!contains("ConfigVersion"))
+ insert("ConfigVersion", "1.2");
+ QSettings _settings_obj{ fileName, QSettings::Format::IniFormat };
+ _settings_obj.setFallbacksEnabled(false);
+
+ for (Iterator iter = begin(); iter != end(); iter++)
+ _settings_obj.setValue(iter.key(), iter.value());
+
+ _settings_obj.sync();
+
+ if (auto status = _settings_obj.status(); status != QSettings::Status::NoError) {
+ // Shouldn't be possible!
+ Q_ASSERT(status != QSettings::Status::FormatError);
+
+ if (status == QSettings::Status::AccessError)
+ qCritical() << "An access error occurred (e.g. trying to write to a read-only file).";
+
+ return false;
+ }
-QString INIFile::unescape(QString orig)
+ return true;
+}
+QString unescape(QString orig)
{
QString out;
QChar prev = QChar::Null;
- for(auto c: orig)
- {
- if(prev == '\\')
- {
- if(c == 'n')
+ for (auto c : orig) {
+ if (prev == '\\') {
+ if (c == 'n')
out += '\n';
- else if(c == 't')
+ else if (c == 't')
out += '\t';
- else if(c == '#')
+ else if (c == '#')
out += '#';
else
out += c;
prev = QChar::Null;
- }
- else
- {
- if(c == '\\')
- {
+ } else {
+ if (c == '\\') {
prev = c;
continue;
}
@@ -78,79 +98,35 @@ QString INIFile::unescape(QString orig)
return out;
}
-QString INIFile::escape(QString orig)
-{
- QString out;
- for(auto c: orig)
- {
- if(c == '\n')
- out += "\\n";
- else if (c == '\t')
- out += "\\t";
- else if(c == '\\')
- out += "\\\\";
- else if(c == '#')
- out += "\\#";
- else
- out += c;
- }
- return out;
-}
-
-bool INIFile::saveFile(QString fileName)
+QString unquote(QString str)
{
- QByteArray outArray;
- for (Iterator iter = begin(); iter != end(); iter++)
- {
- QString value = iter.value().toString();
- value = escape(value);
- outArray.append(iter.key().toUtf8());
- outArray.append('=');
- outArray.append(value.toUtf8());
- outArray.append('\n');
- }
-
- try
- {
- FS::write(fileName, outArray);
- }
- catch (const Exception &e)
- {
- qCritical() << e.what();
- return false;
+ if ((str.contains(QChar(';')) || str.contains(QChar('=')) || str.contains(QChar(','))) && str.endsWith("\"") && str.startsWith("\"")) {
+#if QT_VERSION < QT_VERSION_CHECK(6, 5, 0)
+ str = str.remove(0, 1);
+ str = str.remove(str.size() - 1, 1);
+#else
+ str = str.removeFirst().removeLast();
+#endif
}
-
- return true;
-}
-
-
-bool INIFile::loadFile(QString fileName)
-{
- QFile file(fileName);
- if (!file.open(QIODevice::ReadOnly))
- return false;
- bool success = loadFile(file.readAll());
- file.close();
- return success;
+ return str;
}
-bool INIFile::loadFile(QByteArray file)
+bool parseOldFileFormat(QIODevice& device, QSettings::SettingsMap& map)
{
- QTextStream in(file);
+ QTextStream in(device.readAll());
#if QT_VERSION <= QT_VERSION_CHECK(6, 0, 0)
in.setCodec("UTF-8");
#endif
QStringList lines = in.readAll().split('\n');
- for (int i = 0; i < lines.count(); i++)
- {
- QString &lineRaw = lines[i];
+ for (int i = 0; i < lines.count(); i++) {
+ QString& lineRaw = lines[i];
// Ignore comments.
int commentIndex = 0;
QString line = lineRaw;
// Search for comments until no more escaped # are available
- while((commentIndex = line.indexOf('#', commentIndex + 1)) != -1) {
- if(commentIndex > 0 && line.at(commentIndex - 1) == '\\') {
+ while ((commentIndex = line.indexOf('#', commentIndex + 1)) != -1) {
+ if (commentIndex > 0 && line.at(commentIndex - 1) == '\\') {
continue;
}
line = line.left(lineRaw.indexOf('#')).trimmed();
@@ -162,15 +138,53 @@ bool INIFile::loadFile(QByteArray file)
QString key = line.left(eqPos).trimmed();
QString valueStr = line.right(line.length() - eqPos - 1).trimmed();
- valueStr = unescape(valueStr);
+ valueStr = unquote(unescape(valueStr));
QVariant value(valueStr);
- this->operator[](key) = value;
+ map.insert(key, value);
}
return true;
}
+bool INIFile::loadFile(QString fileName)
+{
+ QSettings _settings_obj{ fileName, QSettings::Format::IniFormat };
+ _settings_obj.setFallbacksEnabled(false);
+
+ if (auto status = _settings_obj.status(); status != QSettings::Status::NoError) {
+ if (status == QSettings::Status::AccessError)
+ qCritical() << "An access error occurred (e.g. trying to write to a read-only file).";
+ if (status == QSettings::Status::FormatError)
+ qCritical() << "A format error occurred (e.g. loading a malformed INI file).";
+ return false;
+ }
+ if (!_settings_obj.value("ConfigVersion").isValid()) {
+ QFile file(fileName);
+ if (!file.open(QIODevice::ReadOnly))
+ return false;
+ QSettings::SettingsMap map;
+ parseOldFileFormat(file, map);
+ file.close();
+ for (auto&& key : map.keys())
+ insert(key, map.value(key));
+ insert("ConfigVersion", "1.2");
+ } else if (_settings_obj.value("ConfigVersion").toString() == "1.1") {
+ for (auto&& key : _settings_obj.allKeys()) {
+ if (auto valueStr = _settings_obj.value(key).toString();
+ (valueStr.contains(QChar(';')) || valueStr.contains(QChar('=')) || valueStr.contains(QChar(','))) &&
+ valueStr.endsWith("\"") && valueStr.startsWith("\"")) {
+ insert(key, unquote(valueStr));
+ } else
+ insert(key, _settings_obj.value(key));
+ }
+ insert("ConfigVersion", "1.2");
+ } else
+ for (auto&& key : _settings_obj.allKeys())
+ insert(key, _settings_obj.value(key));
+ return true;
+}
+
QVariant INIFile::get(QString key, QVariant def) const
{
if (!this->contains(key))
diff --git a/launcher/settings/INIFile.h b/launcher/settings/INIFile.h
index 4313e829..0d5c05eb 100644
--- a/launcher/settings/INIFile.h
+++ b/launcher/settings/INIFile.h
@@ -1,16 +1,37 @@
-/* Copyright 2013-2021 MultiMC Contributors
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ * Copyright (C) 2023 flowln <flowlnlnln@gmail.com>
*
- * 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
+ * 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.
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
*
- * Unless required by applicable law or agreed to in writing, software
- * 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.
+ * 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
@@ -19,18 +40,18 @@
#include <QVariant>
#include <QIODevice>
+#include <QJsonDocument>
+#include <QJsonArray>
+
// Sectionless INI parser (for instance config files)
class INIFile : public QMap<QString, QVariant>
{
public:
explicit INIFile();
- bool loadFile(QByteArray file);
bool loadFile(QString fileName);
bool saveFile(QString fileName);
QVariant get(QString key, QVariant def) const;
void set(QString key, QVariant val);
- static QString unescape(QString orig);
- static QString escape(QString orig);
};
diff --git a/launcher/settings/SettingsObject.cpp b/launcher/settings/SettingsObject.cpp
index 8a0bc045..634acd34 100644
--- a/launcher/settings/SettingsObject.cpp
+++ b/launcher/settings/SettingsObject.cpp
@@ -132,11 +132,10 @@ bool SettingsObject::reload()
void SettingsObject::connectSignals(const Setting &setting)
{
- connect(&setting, SIGNAL(SettingChanged(const Setting &, QVariant)),
- SLOT(changeSetting(const Setting &, QVariant)));
- connect(&setting, SIGNAL(SettingChanged(const Setting &, QVariant)),
+ connect(&setting, &Setting::SettingChanged, this, &SettingsObject::changeSetting);
+ connect(&setting, SIGNAL(SettingChanged(const Setting &, QVariant)), this,
SIGNAL(SettingChanged(const Setting &, QVariant)));
- connect(&setting, SIGNAL(settingReset(Setting)), SLOT(resetSetting(const Setting &)));
- connect(&setting, SIGNAL(settingReset(Setting)), SIGNAL(settingReset(const Setting &)));
+ connect(&setting, &Setting::settingReset, this, &SettingsObject::resetSetting);
+ connect(&setting, SIGNAL(settingReset(Setting)), this, SIGNAL(settingReset(const Setting &)));
}
diff --git a/launcher/settings/SettingsObject.h b/launcher/settings/SettingsObject.h
index 6200bc3a..4d735511 100644
--- a/launcher/settings/SettingsObject.h
+++ b/launcher/settings/SettingsObject.h
@@ -19,6 +19,8 @@
#include <QMap>
#include <QStringList>
#include <QVariant>
+#include <QJsonDocument>
+#include <QJsonArray>
#include <memory>
class Setting;
diff --git a/launcher/tasks/ConcurrentTask.cpp b/launcher/tasks/ConcurrentTask.cpp
index 3cc37b2a..9aada5e6 100644
--- a/launcher/tasks/ConcurrentTask.cpp
+++ b/launcher/tasks/ConcurrentTask.cpp
@@ -1,11 +1,49 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
+ * Copyright (c) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * 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 "ConcurrentTask.h"
-#include <QDebug>
#include <QCoreApplication>
+#include <QDebug>
+#include "tasks/Task.h"
ConcurrentTask::ConcurrentTask(QObject* parent, QString task_name, int max_concurrent)
: Task(parent), m_name(task_name), m_total_max_size(max_concurrent)
-{ setObjectName(task_name); }
+{
+ setObjectName(task_name);
+}
ConcurrentTask::~ConcurrentTask()
{
@@ -15,14 +53,9 @@ ConcurrentTask::~ConcurrentTask()
}
}
-auto ConcurrentTask::getStepProgress() const -> qint64
+auto ConcurrentTask::getStepProgress() const -> TaskStepProgressList
{
- return m_stepProgress;
-}
-
-auto ConcurrentTask::getStepTotalProgress() const -> qint64
-{
- return m_stepTotalProgress;
+ return m_task_progress.values();
}
void ConcurrentTask::addTask(Task::Ptr task)
@@ -32,11 +65,9 @@ void ConcurrentTask::addTask(Task::Ptr task)
void ConcurrentTask::executeTask()
{
- // Start the least amount of tasks needed, but at least one
- int num_starts = qMax(1, qMin(m_total_max_size, m_queue.size()));
- for (int i = 0; i < num_starts; i++) {
- QMetaObject::invokeMethod(this, &ConcurrentTask::startNext, Qt::QueuedConnection);
- }
+ // Start one task, startNext handles starting the up to the m_total_max_size
+ // while tracking the number currently being done
+ QMetaObject::invokeMethod(this, &ConcurrentTask::startNext, Qt::QueuedConnection);
}
bool ConcurrentTask::abort()
@@ -97,25 +128,28 @@ void ConcurrentTask::startNext()
Task::Ptr next = m_queue.dequeue();
- connect(next.get(), &Task::succeeded, this, [this, next] { subTaskSucceeded(next); });
+ connect(next.get(), &Task::succeeded, this, [this, next]() { subTaskSucceeded(next); });
connect(next.get(), &Task::failed, this, [this, next](QString msg) { subTaskFailed(next, msg); });
- connect(next.get(), &Task::status, this, &ConcurrentTask::subTaskStatus);
- connect(next.get(), &Task::stepStatus, this, &ConcurrentTask::subTaskStatus);
+ connect(next.get(), &Task::status, this, [this, next](QString msg) { subTaskStatus(next, msg); });
+ connect(next.get(), &Task::details, this, [this, next](QString msg) { subTaskDetails(next, msg); });
+ connect(next.get(), &Task::stepProgress, this, [this, next](TaskStepProgress const& tp) { subTaskStepProgress(next, tp); });
- connect(next.get(), &Task::progress, this, &ConcurrentTask::subTaskProgress);
+ connect(next.get(), &Task::progress, this, [this, next](qint64 current, qint64 total) { subTaskProgress(next, current, total); });
m_doing.insert(next.get(), next);
+ qsizetype num_starts = qMin(m_queue.size(), m_total_max_size - m_doing.size());
+ auto task_progress = std::make_shared<TaskStepProgress>(next->getUid());
+ m_task_progress.insert(next->getUid(), task_progress);
- setStepStatus(next->isMultiStep() ? next->getStepStatus() : next->getStatus());
updateState();
+ updateStepProgress(*task_progress.get(), Operation::ADDED);
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 = 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);
}
@@ -123,12 +157,17 @@ void ConcurrentTask::startNext()
void ConcurrentTask::subTaskSucceeded(Task::Ptr task)
{
m_done.insert(task.get(), task);
+ m_succeeded.insert(task.get(), task);
+
m_doing.remove(task.get());
+ auto task_progress = m_task_progress.value(task->getUid());
+ task_progress->state = TaskStepState::Succeeded;
disconnect(task.get(), 0, this, 0);
+ emit stepProgress(*task_progress);
updateState();
-
+ updateStepProgress(*task_progress, Operation::REMOVED);
startNext();
}
@@ -139,27 +178,123 @@ void ConcurrentTask::subTaskFailed(Task::Ptr task, const QString& msg)
m_doing.remove(task.get());
+ auto task_progress = m_task_progress.value(task->getUid());
+ task_progress->state = TaskStepState::Failed;
+
disconnect(task.get(), 0, this, 0);
+ emit stepProgress(*task_progress);
updateState();
-
+ updateStepProgress(*task_progress, Operation::REMOVED);
startNext();
}
-void ConcurrentTask::subTaskStatus(const QString& msg)
+void ConcurrentTask::subTaskStatus(Task::Ptr task, const QString& msg)
+{
+ auto task_progress = m_task_progress.value(task->getUid());
+ task_progress->status = msg;
+ task_progress->state = TaskStepState::Running;
+
+ emit stepProgress(*task_progress);
+
+ if (totalSize() == 1) {
+ setStatus(msg);
+ }
+}
+
+void ConcurrentTask::subTaskDetails(Task::Ptr task, const QString& msg)
{
- setStepStatus(msg);
+ auto task_progress = m_task_progress.value(task->getUid());
+ task_progress->details = msg;
+ task_progress->state = TaskStepState::Running;
+
+ emit stepProgress(*task_progress);
+
+ if (totalSize() == 1) {
+ setDetails(msg);
+ }
}
-void ConcurrentTask::subTaskProgress(qint64 current, qint64 total)
+void ConcurrentTask::subTaskProgress(Task::Ptr task, qint64 current, qint64 total)
{
- m_stepProgress = current;
- m_stepTotalProgress = total;
+ auto task_progress = m_task_progress.value(task->getUid());
+
+ task_progress->update(current, total);
+
+ emit stepProgress(*task_progress);
+ updateStepProgress(*task_progress, Operation::CHANGED);
+ updateState();
+
+ if (totalSize() == 1) {
+ setProgress(task_progress->current, task_progress->total);
+ }
+}
+
+void ConcurrentTask::subTaskStepProgress(Task::Ptr task, TaskStepProgress const& task_progress)
+{
+ Operation op = Operation::ADDED;
+
+ if (!m_task_progress.contains(task_progress.uid)) {
+ m_task_progress.insert(task_progress.uid, std::make_shared<TaskStepProgress>(task_progress));
+ op = Operation::ADDED;
+ emit stepProgress(task_progress);
+ updateStepProgress(task_progress, op);
+ } else {
+ auto tp = m_task_progress.value(task_progress.uid);
+
+ tp->old_current = tp->current;
+ tp->old_total = tp->total;
+
+ tp->current = task_progress.current;
+ tp->total = task_progress.total;
+ tp->status = task_progress.status;
+ tp->details = task_progress.details;
+
+ op = Operation::CHANGED;
+ emit stepProgress(*tp.get());
+ updateStepProgress(*tp.get(), op);
+ }
+
+}
+
+void ConcurrentTask::updateStepProgress(TaskStepProgress const& changed_progress, Operation op)
+{
+
+ switch (op) {
+ case Operation::ADDED:
+ m_stepProgress += changed_progress.current;
+ m_stepTotalProgress += changed_progress.total;
+ break;
+ case Operation::REMOVED:
+ m_stepProgress -= changed_progress.current;
+ m_stepTotalProgress -= changed_progress.total;
+ break;
+ case Operation::CHANGED:
+ m_stepProgress -= changed_progress.old_current;
+ m_stepTotalProgress -= changed_progress.old_total;
+ m_stepProgress += changed_progress.current;
+ m_stepTotalProgress += changed_progress.total;
+ break;
+ }
+
}
void ConcurrentTask::updateState()
{
- setProgress(m_done.count(), totalSize());
- setStatus(tr("Executing %1 task(s) (%2 out of %3 are done)")
- .arg(QString::number(m_doing.count()), QString::number(m_done.count()), QString::number(totalSize())));
+ if (totalSize() > 1) {
+ setProgress(m_done.count(), totalSize());
+ setStatus(tr("Executing %1 task(s) (%2 out of %3 are done)")
+ .arg(QString::number(m_doing.count()), QString::number(m_done.count()), QString::number(totalSize())));
+ } else {
+ setProgress(m_stepProgress, m_stepTotalProgress);
+ QString status = tr("Please wait...");
+ if (m_queue.size() > 0) {
+ status = tr("Waiting for a task to start...");
+ } else if (m_doing.size() > 0) {
+ status = tr("Executing 1 task:");
+ } else if (m_done.size() > 0) {
+ status = tr("Task finished.");
+ }
+ setStatus(status);
+ }
}
diff --git a/launcher/tasks/ConcurrentTask.h b/launcher/tasks/ConcurrentTask.h
index d074d2e2..6325fc9e 100644
--- a/launcher/tasks/ConcurrentTask.h
+++ b/launcher/tasks/ConcurrentTask.h
@@ -1,13 +1,51 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
+ * Copyright (c) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * 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 <QHash>
#include <QQueue>
#include <QSet>
+#include <QUuid>
+#include <memory>
#include "tasks/Task.h"
class ConcurrentTask : public Task {
Q_OBJECT
-public:
+ public:
using Ptr = shared_qobject_ptr<ConcurrentTask>;
explicit ConcurrentTask(QObject* parent = nullptr, QString task_name = "", int max_concurrent = 6);
@@ -15,15 +53,12 @@ public:
bool canAbort() const override { return true; }
- inline auto isMultiStep() const -> bool override { return m_queue.size() > 1; };
- auto getStepProgress() const -> qint64 override;
- auto getStepTotalProgress() const -> qint64 override;
-
- inline auto getStepStatus() const -> QString override { return m_step_status; }
+ inline auto isMultiStep() const -> bool override { return totalSize() > 1; };
+ auto getStepProgress() const -> TaskStepProgressList override;
void addTask(Task::Ptr task);
-public slots:
+ public slots:
bool abort() override;
/** Resets the internal state of the task.
@@ -31,26 +66,28 @@ public slots:
*/
void clear();
-protected
-slots:
+ protected slots:
void executeTask() override;
virtual void startNext();
void subTaskSucceeded(Task::Ptr);
- void subTaskFailed(Task::Ptr, const QString &msg);
- void subTaskStatus(const QString &msg);
- void subTaskProgress(qint64 current, qint64 total);
+ void subTaskFailed(Task::Ptr, const QString& msg);
+ void subTaskStatus(Task::Ptr task, const QString& msg);
+ void subTaskDetails(Task::Ptr task, const QString& msg);
+ void subTaskProgress(Task::Ptr task, qint64 current, qint64 total);
+ void subTaskStepProgress(Task::Ptr task, TaskStepProgress const& task_step_progress);
-protected:
+ protected:
// NOTE: This is not thread-safe.
[[nodiscard]] unsigned int totalSize() const { return m_queue.size() + m_doing.size() + m_done.size(); }
- void setStepStatus(QString status) { m_step_status = status; emit stepStatus(status); };
+ enum class Operation { ADDED, REMOVED, CHANGED };
+ void updateStepProgress(TaskStepProgress const& changed_progress, Operation);
virtual void updateState();
-protected:
+ protected:
QString m_name;
QString m_step_status;
@@ -59,6 +96,9 @@ protected:
QHash<Task*, Task::Ptr> m_doing;
QHash<Task*, Task::Ptr> m_done;
QHash<Task*, Task::Ptr> m_failed;
+ QHash<Task*, Task::Ptr> m_succeeded;
+
+ QHash<QUuid, std::shared_ptr<TaskStepProgress>> m_task_progress;
int m_total_max_size;
diff --git a/launcher/tasks/MultipleOptionsTask.cpp b/launcher/tasks/MultipleOptionsTask.cpp
index 034499df..89187a26 100644
--- a/launcher/tasks/MultipleOptionsTask.cpp
+++ b/launcher/tasks/MultipleOptionsTask.cpp
@@ -1,3 +1,37 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - 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/>.
+ *
+ * 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 "MultipleOptionsTask.h"
#include <QDebug>
diff --git a/launcher/tasks/MultipleOptionsTask.h b/launcher/tasks/MultipleOptionsTask.h
index db7d4d9a..a344343e 100644
--- a/launcher/tasks/MultipleOptionsTask.h
+++ b/launcher/tasks/MultipleOptionsTask.h
@@ -1,3 +1,37 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - 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/>.
+ *
+ * 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 "SequentialTask.h"
diff --git a/launcher/tasks/SequentialTask.cpp b/launcher/tasks/SequentialTask.cpp
index b2f86328..abf7536b 100644
--- a/launcher/tasks/SequentialTask.cpp
+++ b/launcher/tasks/SequentialTask.cpp
@@ -1,3 +1,38 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
+ * Copyright (c) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * 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 "SequentialTask.h"
#include <QDebug>
diff --git a/launcher/tasks/SequentialTask.h b/launcher/tasks/SequentialTask.h
index 5eace96e..cec3b2be 100644
--- a/launcher/tasks/SequentialTask.h
+++ b/launcher/tasks/SequentialTask.h
@@ -1,3 +1,38 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
+ * Copyright (c) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * 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 "ConcurrentTask.h"
diff --git a/launcher/tasks/Task.cpp b/launcher/tasks/Task.cpp
index 9ea1bb26..29c55cd4 100644
--- a/launcher/tasks/Task.cpp
+++ b/launcher/tasks/Task.cpp
@@ -2,6 +2,7 @@
/*
* PolyMC - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
+ * Copyright (c) 2023 Rachel Powers <508861+Ryex@users.noreply.github.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
@@ -37,8 +38,11 @@
#include <QDebug>
+Q_LOGGING_CATEGORY(taskLogC, "launcher.task")
+
Task::Task(QObject *parent, bool show_debug) : QObject(parent), m_show_debug(show_debug)
{
+ m_uid = QUuid::createUuid();
setAutoDelete(false);
}
@@ -51,11 +55,23 @@ void Task::setStatus(const QString &new_status)
}
}
+void Task::setDetails(const QString& new_details)
+{
+ if (m_details != new_details)
+ {
+ m_details = new_details;
+ emit details(m_details);
+ }
+}
+
void Task::setProgress(qint64 current, qint64 total)
{
- m_progress = current;
- m_progressTotal = total;
- emit progress(m_progress, m_progressTotal);
+ if ((m_progress != current) || (m_progressTotal != total)) {
+ m_progress = current;
+ m_progressTotal = total;
+
+ emit progress(m_progress, m_progressTotal);
+ }
}
void Task::start()
@@ -65,35 +81,35 @@ void Task::start()
case State::Inactive:
{
if (m_show_debug)
- qDebug() << "Task" << describe() << "starting for the first time";
+ qCDebug(taskLogC) << "Task" << describe() << "starting for the first time";
break;
}
case State::AbortedByUser:
{
if (m_show_debug)
- qDebug() << "Task" << describe() << "restarting for after being aborted by user";
+ qCDebug(taskLogC) << "Task" << describe() << "restarting for after being aborted by user";
break;
}
case State::Failed:
{
if (m_show_debug)
- qDebug() << "Task" << describe() << "restarting for after failing at first";
+ qCDebug(taskLogC) << "Task" << describe() << "restarting for after failing at first";
break;
}
case State::Succeeded:
{
if (m_show_debug)
- qDebug() << "Task" << describe() << "restarting for after succeeding at first";
+ qCDebug(taskLogC) << "Task" << describe() << "restarting for after succeeding at first";
break;
}
case State::Running:
{
if (m_show_debug)
- qWarning() << "The launcher tried to start task" << describe() << "while it was already running!";
+ qCWarning(taskLogC) << "The launcher tried to start task" << describe() << "while it was already running!";
return;
}
}
- // NOTE: only fall thorugh to here in end states
+ // NOTE: only fall through to here in end states
m_state = State::Running;
emit started();
executeTask();
@@ -104,12 +120,12 @@ void Task::emitFailed(QString reason)
// Don't fail twice.
if (!isRunning())
{
- qCritical() << "Task" << describe() << "failed while not running!!!!: " << reason;
+ qCCritical(taskLogC) << "Task" << describe() << "failed while not running!!!!: " << reason;
return;
}
m_state = State::Failed;
m_failReason = reason;
- qCritical() << "Task" << describe() << "failed: " << reason;
+ qCCritical(taskLogC) << "Task" << describe() << "failed: " << reason;
emit failed(reason);
emit finished();
}
@@ -119,13 +135,13 @@ void Task::emitAborted()
// Don't abort twice.
if (!isRunning())
{
- qCritical() << "Task" << describe() << "aborted while not running!!!!";
+ qCCritical(taskLogC) << "Task" << describe() << "aborted while not running!!!!";
return;
}
m_state = State::AbortedByUser;
m_failReason = "Aborted.";
if (m_show_debug)
- qDebug() << "Task" << describe() << "aborted.";
+ qCDebug(taskLogC) << "Task" << describe() << "aborted.";
emit aborted();
emit finished();
}
@@ -135,16 +151,21 @@ void Task::emitSucceeded()
// Don't succeed twice.
if (!isRunning())
{
- qCritical() << "Task" << describe() << "succeeded while not running!!!!";
+ qCCritical(taskLogC) << "Task" << describe() << "succeeded while not running!!!!";
return;
}
m_state = State::Succeeded;
if (m_show_debug)
- qDebug() << "Task" << describe() << "succeeded";
+ qCDebug(taskLogC) << "Task" << describe() << "succeeded";
emit succeeded();
emit finished();
}
+void Task::propogateStepProgress(TaskStepProgress const& task_progress)
+{
+ emit stepProgress(task_progress);
+}
+
QString Task::describe()
{
QString outStr;
@@ -159,6 +180,7 @@ QString Task::describe()
{
out << name;
}
+ out << " ID: " << m_uid.toString(QUuid::WithoutBraces);
out << QChar(')');
out.flush();
return outStr;
diff --git a/launcher/tasks/Task.h b/launcher/tasks/Task.h
index 3d607dca..6d8bbbb4 100644
--- a/launcher/tasks/Task.h
+++ b/launcher/tasks/Task.h
@@ -1,7 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
- * PolyMC - Minecraft Launcher
+ * PrismLauncher - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
+ * Copyright (c) 2023 Rachel Powers <508861+Ryex@users.noreply.github.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
@@ -36,9 +37,54 @@
#pragma once
#include <QRunnable>
+#include <QUuid>
+#include <QLoggingCategory>
#include "QObjectPtr.h"
+Q_DECLARE_LOGGING_CATEGORY(taskLogC)
+
+enum class TaskStepState {
+ Waiting,
+ Running,
+ Failed,
+ Succeeded
+};
+
+Q_DECLARE_METATYPE(TaskStepState)
+
+struct TaskStepProgress {
+ QUuid uid;
+ qint64 current = 0;
+ qint64 total = -1;
+
+ qint64 old_current = 0;
+ qint64 old_total = -1;
+
+ QString status = "";
+ QString details = "";
+ TaskStepState state = TaskStepState::Waiting;
+ TaskStepProgress() {
+ this->uid = QUuid::createUuid();
+ }
+ TaskStepProgress(QUuid uid) {
+ this->uid = uid;
+ }
+ bool isDone() const { return (state == TaskStepState::Failed) || (state == TaskStepState::Succeeded); }
+ void update(qint64 current, qint64 total) {
+ this->old_current = this->current;
+ this->old_total = this->total;
+
+ this->current = current;
+ this->total = total;
+ this->state = TaskStepState::Running;
+ }
+};
+
+Q_DECLARE_METATYPE(TaskStepProgress)
+
+typedef QList<std::shared_ptr<TaskStepProgress>> TaskStepProgressList;
+
class Task : public QObject, public QRunnable {
Q_OBJECT
public:
@@ -73,12 +119,15 @@ class Task : public QObject, public QRunnable {
auto getState() const -> State { return m_state; }
QString getStatus() { return m_status; }
- virtual auto getStepStatus() const -> QString { return m_status; }
+ QString getDetails() { return m_details; }
qint64 getProgress() { return m_progress; }
qint64 getTotalProgress() { return m_progressTotal; }
- virtual auto getStepProgress() const -> qint64 { return 0; }
- virtual auto getStepTotalProgress() const -> qint64 { return 100; }
+ virtual auto getStepProgress() const -> TaskStepProgressList { return {}; }
+
+
+
+ QUuid getUid() { return m_uid; }
protected:
void logWarning(const QString& line);
@@ -94,7 +143,8 @@ class Task : public QObject, public QRunnable {
void aborted();
void failed(QString reason);
void status(QString status);
- void stepStatus(QString status);
+ void details(QString details);
+ void stepProgress(TaskStepProgress const& task_progress);
/** Emitted when the canAbort() status has changed.
*/
@@ -117,8 +167,11 @@ class Task : public QObject, public QRunnable {
virtual void emitAborted();
virtual void emitFailed(QString reason = "");
+ virtual void propogateStepProgress(TaskStepProgress const& task_progress);
+
public slots:
void setStatus(const QString& status);
+ void setDetails(const QString& details);
void setProgress(qint64 current, qint64 total);
protected:
@@ -126,6 +179,7 @@ class Task : public QObject, public QRunnable {
QStringList m_Warnings;
QString m_failReason = "";
QString m_status;
+ QString m_details;
int m_progress = 0;
int m_progressTotal = 100;
@@ -135,4 +189,6 @@ class Task : public QObject, public QRunnable {
private:
// Change using setAbortStatus
bool m_can_abort = false;
+ QUuid m_uid;
+
};
diff --git a/launcher/tools/JProfiler.cpp b/launcher/tools/JProfiler.cpp
index 1dc0d109..15c0cab6 100644
--- a/launcher/tools/JProfiler.cpp
+++ b/launcher/tools/JProfiler.cpp
@@ -68,8 +68,8 @@ void JProfiler::beginProfilingImpl(shared_qobject_ptr<LaunchTask> process)
profiler->setArguments(profilerArgs);
profiler->setProgram(profilerProgram);
- connect(profiler, SIGNAL(started()), SLOT(profilerStarted()));
- connect(profiler, SIGNAL(finished(int, QProcess::ExitStatus)), SLOT(profilerFinished(int,QProcess::ExitStatus)));
+ connect(profiler, &QProcess::started, this, &JProfiler::profilerStarted);
+ connect(profiler, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this, &JProfiler::profilerFinished);
m_profilerProcess = profiler;
profiler->start();
diff --git a/launcher/tools/JVisualVM.cpp b/launcher/tools/JVisualVM.cpp
index b1acc3c0..28ffb9cd 100644
--- a/launcher/tools/JVisualVM.cpp
+++ b/launcher/tools/JVisualVM.cpp
@@ -57,8 +57,8 @@ void JVisualVM::beginProfilingImpl(shared_qobject_ptr<LaunchTask> process)
profiler->setArguments(profilerArgs);
profiler->setProgram(programPath);
- connect(profiler, SIGNAL(started()), SLOT(profilerStarted()));
- connect(profiler, SIGNAL(finished(int, QProcess::ExitStatus)), SLOT(profilerFinished(int,QProcess::ExitStatus)));
+ connect(profiler, &QProcess::started, this, &JVisualVM::profilerStarted);
+ connect(profiler, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this, &JVisualVM::profilerFinished);
profiler->start();
m_profilerProcess = profiler;
diff --git a/launcher/translations/TranslationsModel.cpp b/launcher/translations/TranslationsModel.cpp
index 46db4804..2763cca2 100644
--- a/launcher/translations/TranslationsModel.cpp
+++ b/launcher/translations/TranslationsModel.cpp
@@ -42,6 +42,7 @@
#include <QDir>
#include <QLibraryInfo>
#include <QDebug>
+#include <locale>
#include "FileSystem.h"
#include "net/NetJob.h"
@@ -190,7 +191,7 @@ struct TranslationsModel::Private
std::unique_ptr<QTranslator> m_qt_translator;
std::unique_ptr<QTranslator> m_app_translator;
- Net::Download::Ptr m_index_task;
+ Net::Download* m_index_task;
QString m_downloadingTranslation;
NetJob::Ptr m_dl_job;
NetJob::Ptr m_index_job;
@@ -454,6 +455,7 @@ QVariant TranslationsModel::data(const QModelIndex& index, int role) const
return QString("%1%").arg(lang.percentTranslated(), 3, 'f', 1);
}
}
+ qWarning("TranslationModel::data not implemented when role is DisplayRole");
}
case Qt::ToolTipRole:
{
@@ -526,34 +528,34 @@ Language * TranslationsModel::findLanguage(const QString& key)
}
}
+void TranslationsModel::setUseSystemLocale(bool useSystemLocale)
+{
+ APPLICATION->settings()->set("UseSystemLocale", useSystemLocale);
+ QLocale::setDefault(QLocale(useSystemLocale ? QString::fromStdString(std::locale().name()) : defaultLangCode));
+}
+
bool TranslationsModel::selectLanguage(QString key)
{
- QString &langCode = key;
+ QString& langCode = key;
auto langPtr = findLanguage(key);
- if (langCode.isEmpty())
- {
+ if (langCode.isEmpty()) {
d->no_language_set = true;
}
- if(!langPtr)
- {
+ if (!langPtr) {
qWarning() << "Selected invalid language" << key << ", defaulting to" << defaultLangCode;
langCode = defaultLangCode;
- }
- else
- {
+ } else {
langCode = langPtr->key;
}
// uninstall existing translators if there are any
- if (d->m_app_translator)
- {
+ if (d->m_app_translator) {
QCoreApplication::removeTranslator(d->m_app_translator.get());
d->m_app_translator.reset();
}
- if (d->m_qt_translator)
- {
+ if (d->m_qt_translator) {
QCoreApplication::removeTranslator(d->m_qt_translator.get());
d->m_qt_translator.reset();
}
@@ -563,8 +565,9 @@ bool TranslationsModel::selectLanguage(QString key)
* In a multithreaded application, the default locale should be set at application startup, before any non-GUI threads are created.
* This function is not reentrant.
*/
- QLocale locale = QLocale(langCode);
- QLocale::setDefault(locale);
+ QLocale::setDefault(
+ QLocale(APPLICATION->settings()->get("UseSystemLocale").toBool() ? QString::fromStdString(std::locale().name()) : langCode));
+
// if it's the default UI language, finish
if(langCode == defaultLangCode)
@@ -673,8 +676,9 @@ void TranslationsModel::downloadIndex()
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);
- d->m_index_job->addNetAction(d->m_index_task);
+ auto task = Net::Download::makeCached(QUrl(BuildConfig.TRANSLATIONS_BASE_URL + "index_v2.json"), entry);
+ d->m_index_task = task.get();
+ d->m_index_job->addNetAction(task);
connect(d->m_index_job.get(), &NetJob::failed, this, &TranslationsModel::indexFailed);
connect(d->m_index_job.get(), &NetJob::succeeded, this, &TranslationsModel::indexReceived);
d->m_index_job->start();
diff --git a/launcher/translations/TranslationsModel.h b/launcher/translations/TranslationsModel.h
index 3abf84e6..cff23ce7 100644
--- a/launcher/translations/TranslationsModel.h
+++ b/launcher/translations/TranslationsModel.h
@@ -20,17 +20,16 @@
struct Language;
-class TranslationsModel : public QAbstractListModel
-{
+class TranslationsModel : public QAbstractListModel {
Q_OBJECT
-public:
- explicit TranslationsModel(QString path, QObject *parent = 0);
+ public:
+ explicit TranslationsModel(QString path, QObject* parent = 0);
virtual ~TranslationsModel();
- QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
+ QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
- int rowCount(const QModelIndex &parent = QModelIndex()) const override;
- int columnCount(const QModelIndex & parent) const override;
+ int rowCount(const QModelIndex& parent = QModelIndex()) const override;
+ int columnCount(const QModelIndex& parent) const override;
bool selectLanguage(QString key);
void updateLanguage(QString key);
@@ -38,27 +37,27 @@ public:
QString selectedLanguage();
void downloadIndex();
+ void setUseSystemLocale(bool useSystemLocale);
-private:
- Language *findLanguage(const QString & key);
+ private:
+ Language* findLanguage(const QString& key);
void reloadLocalFiles();
void downloadTranslation(QString key);
void downloadNext();
// hide copy constructor
- TranslationsModel(const TranslationsModel &) = delete;
+ TranslationsModel(const TranslationsModel&) = delete;
// hide assign op
- TranslationsModel &operator=(const TranslationsModel &) = delete;
+ TranslationsModel& operator=(const TranslationsModel&) = delete;
-private slots:
+ private slots:
void indexReceived();
void indexFailed(QString reason);
void dlFailed(QString reason);
void dlGood();
- void translationDirChanged(const QString &path);
+ void translationDirChanged(const QString& path);
-
-private: /* data */
+ private: /* data */
struct Private;
std::unique_ptr<Private> d;
};
diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp
index 8490b292..da572fc3 100644
--- a/launcher/ui/MainWindow.cpp
+++ b/launcher/ui/MainWindow.cpp
@@ -2,7 +2,7 @@
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
- * Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
+ * Copyright (C) 2023 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
@@ -43,101 +43,100 @@
#include "FileSystem.h"
#include "MainWindow.h"
+#include "ui/dialogs/ExportToModListDialog.h"
#include "ui_MainWindow.h"
-#include <QVariant>
-#include <QUrl>
#include <QDir>
#include <QFileInfo>
+#include <QUrl>
+#include <QVariant>
-#include <QKeyEvent>
#include <QAction>
#include <QActionGroup>
#include <QApplication>
#include <QButtonGroup>
+#include <QFileDialog>
#include <QHBoxLayout>
#include <QHeaderView>
+#include <QInputDialog>
+#include <QKeyEvent>
+#include <QLabel>
#include <QMainWindow>
-#include <QStatusBar>
-#include <QToolBar>
-#include <QWidget>
#include <QMenu>
#include <QMenuBar>
#include <QMessageBox>
-#include <QFileDialog>
-#include <QInputDialog>
-#include <QLabel>
-#include <QToolButton>
-#include <QWidgetAction>
#include <QProgressDialog>
#include <QShortcut>
+#include <QStatusBar>
+#include <QToolBar>
+#include <QToolButton>
+#include <QWidget>
+#include <QWidgetAction>
#include <BaseInstance.h>
+#include <BuildConfig.h>
+#include <DesktopServices.h>
#include <InstanceList.h>
-#include <minecraft/MinecraftInstance.h>
#include <MMCZip.h>
+#include <SkinUtils.h>
#include <icons/IconList.h>
-#include <java/JavaUtils.h>
#include <java/JavaInstallList.h>
+#include <java/JavaUtils.h>
#include <launch/LaunchTask.h>
+#include <minecraft/MinecraftInstance.h>
#include <minecraft/auth/AccountList.h>
-#include <SkinUtils.h>
-#include <BuildConfig.h>
-#include <net/NetJob.h>
#include <net/Download.h>
+#include <net/NetJob.h>
#include <news/NewsChecker.h>
#include <tools/BaseProfiler.h>
#include <updater/ExternalUpdater.h>
-#include <DesktopServices.h>
-#include "InstanceWindow.h"
#include "InstancePageProvider.h"
+#include "InstanceWindow.h"
#include "JavaCommon.h"
#include "LaunchController.h"
-#include "ui/instanceview/InstanceProxyModel.h"
-#include "ui/instanceview/InstanceView.h"
-#include "ui/instanceview/InstanceDelegate.h"
-#include "ui/widgets/LabeledToolButton.h"
-#include "ui/dialogs/NewInstanceDialog.h"
-#include "ui/dialogs/NewsDialog.h"
-#include "ui/dialogs/ProgressDialog.h"
#include "ui/dialogs/AboutDialog.h"
-#include "ui/dialogs/CustomMessageBox.h"
-#include "ui/dialogs/IconPickerDialog.h"
#include "ui/dialogs/CopyInstanceDialog.h"
+#include "ui/dialogs/CustomMessageBox.h"
#include "ui/dialogs/EditAccountDialog.h"
#include "ui/dialogs/ExportInstanceDialog.h"
+#include "ui/dialogs/ExportPackDialog.h"
+#include "ui/dialogs/IconPickerDialog.h"
#include "ui/dialogs/ImportResourceDialog.h"
+#include "ui/dialogs/NewInstanceDialog.h"
+#include "ui/dialogs/NewsDialog.h"
+#include "ui/dialogs/ProgressDialog.h"
+#include "ui/instanceview/InstanceDelegate.h"
+#include "ui/instanceview/InstanceProxyModel.h"
+#include "ui/instanceview/InstanceView.h"
#include "ui/themes/ITheme.h"
#include "ui/themes/ThemeManager.h"
+#include "ui/widgets/LabeledToolButton.h"
-#include "minecraft/mod/tasks/LocalResourceParse.h"
+#include "minecraft/WorldList.h"
#include "minecraft/mod/ModFolderModel.h"
#include "minecraft/mod/ShaderPackFolderModel.h"
-#include "minecraft/WorldList.h"
+#include "minecraft/mod/tasks/LocalResourceParse.h"
#include "KonamiCode.h"
-#include "InstanceImportTask.h"
#include "InstanceCopyTask.h"
+#include "InstanceImportTask.h"
#include "MMCTime.h"
namespace {
-QString profileInUseFilter(const QString & profile, bool used)
+QString profileInUseFilter(const QString& profile, bool used)
{
- if(used)
- {
+ if (used) {
return QObject::tr("%1 (in use)").arg(profile);
- }
- else
- {
+ } else {
return profile;
}
}
-}
+} // namespace
-MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow)
+MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWindow)
{
ui->setupUi(this);
@@ -183,10 +182,9 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi
ui->instanceToolBar->addContextMenuAction(ui->newsToolBar->toggleViewAction());
ui->instanceToolBar->addContextMenuAction(ui->instanceToolBar->toggleViewAction());
ui->instanceToolBar->addContextMenuAction(ui->actionLockToolbars);
-
}
- // set the menu for the folders help, and accounts tool buttons
+ // set the menu for the folders help, accounts, and export tool buttons
{
auto foldersMenuButton = dynamic_cast<QToolButton*>(ui->mainToolBar->widgetForAction(ui->actionFoldersButton));
ui->actionFoldersButton->setMenu(ui->foldersMenu);
@@ -199,8 +197,14 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi
helpMenuButton->setPopupMode(QToolButton::InstantPopup);
auto accountMenuButton = dynamic_cast<QToolButton*>(ui->mainToolBar->widgetForAction(ui->actionAccountsButton));
- ui->actionAccountsButton->setMenu(ui->accountsMenu);
accountMenuButton->setPopupMode(QToolButton::InstantPopup);
+
+ auto exportInstanceMenu = new QMenu(this);
+ exportInstanceMenu->addAction(ui->actionExportInstanceZip);
+ exportInstanceMenu->addAction(ui->actionExportInstanceMrPack);
+ exportInstanceMenu->addAction(ui->actionExportInstanceFlamePack);
+ exportInstanceMenu->addAction(ui->actionExportInstanceToModList);
+ ui->actionExportInstance->setMenu(exportInstanceMenu);
}
// hide, disable and show stuff
@@ -219,6 +223,12 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi
// disabled until we have an instance selected
ui->instanceToolBar->setEnabled(false);
setInstanceActionsEnabled(false);
+
+ // add a close button at the end of the main toolbar when running on gamescope / steam deck
+ // FIXME: detect if we don't have server side decorations instead
+ if (qgetenv("XDG_CURRENT_DESKTOP") == "gamescope") {
+ ui->mainToolBar->addAction(ui->actionCloseWindow);
+ }
}
// add the toolbar toggles to the view menu
@@ -288,9 +298,8 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi
connect(proxymodel, &InstanceProxyModel::dataChanged, this, &MainWindow::instanceDataChanged);
view->setModel(proxymodel);
- view->setSourceOfGroupCollapseStatus([](const QString & groupName)->bool {
- return APPLICATION->instances()->isGroupCollapsed(groupName);
- });
+ view->setSourceOfGroupCollapseStatus(
+ [](const QString& groupName) -> bool { return APPLICATION->instances()->isGroupCollapsed(groupName); });
connect(view, &InstanceView::groupStateChanged, APPLICATION->instances().get(), &InstanceList::on_GroupStateChanged);
ui->horizontalLayout->addWidget(view);
}
@@ -336,7 +345,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi
statusBar()->addPermanentWidget(m_statusCenter, 0);
// Add "manage accounts" button, right align
- QWidget *spacer = new QWidget();
+ QWidget* spacer = new QWidget();
spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
ui->mainToolBar->insertWidget(ui->actionAccountsButton, spacer);
@@ -348,21 +357,8 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi
// 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...
- connect(
- APPLICATION->accounts().get(),
- &AccountList::defaultAccountChanged,
- [this] {
- defaultAccountChanged();
- }
- );
- connect(
- APPLICATION->accounts().get(),
- &AccountList::listChanged,
- [this]
- {
- repopulateAccountsMenu();
- }
- );
+ connect(APPLICATION->accounts().get(), &AccountList::defaultAccountChanged, [this] { defaultAccountChanged(); });
+ connect(APPLICATION->accounts().get(), &AccountList::listChanged, [this] { repopulateAccountsMenu(); });
// Show initial account
defaultAccountChanged();
@@ -403,9 +399,9 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi
// macOS always has a native menu bar, so these fixes are not applicable
// Other systems may or may not have a native menu bar (most do not - it seems like only Ubuntu Unity does)
#ifndef Q_OS_MAC
-void MainWindow::keyReleaseEvent(QKeyEvent *event)
+void MainWindow::keyReleaseEvent(QKeyEvent* event)
{
- if(event->key()==Qt::Key_Alt && !APPLICATION->settings()->get("MenuBarInsteadOfToolBar").toBool())
+ if (event->key() == Qt::Key_Alt && !APPLICATION->settings()->get("MenuBarInsteadOfToolBar").toBool())
ui->menuBar->setVisible(!ui->menuBar->isVisible());
else
QMainWindow::keyReleaseEvent(event);
@@ -414,16 +410,6 @@ void MainWindow::keyReleaseEvent(QKeyEvent *event)
void MainWindow::retranslateUi()
{
- auto accounts = APPLICATION->accounts();
- MinecraftAccountPtr defaultAccount = accounts->defaultAccount();
- if(defaultAccount) {
- auto profileLabel = profileInUseFilter(defaultAccount->profileName(), defaultAccount->isInUse());
- ui->actionAccountsButton->setText(profileLabel);
- }
- else {
- ui->actionAccountsButton->setText(tr("Accounts"));
- }
-
if (m_selectedInstance) {
m_statusLeft->setText(m_selectedInstance->getStatusbarDescription());
} else {
@@ -432,6 +418,12 @@ void MainWindow::retranslateUi()
ui->retranslateUi(this);
+ MinecraftAccountPtr defaultAccount = APPLICATION->accounts()->defaultAccount();
+ if (defaultAccount) {
+ auto profileLabel = profileInUseFilter(defaultAccount->profileName(), defaultAccount->isInUse());
+ ui->actionAccountsButton->setText(profileLabel);
+ }
+
changeIconButton->setToolTip(ui->actionChangeInstIcon->toolTip());
renameButton->setToolTip(ui->actionRenameInstance->toolTip());
@@ -447,14 +439,12 @@ void MainWindow::retranslateUi()
}
}
-MainWindow::~MainWindow()
-{
-}
+MainWindow::~MainWindow() {}
-QMenu * MainWindow::createPopupMenu()
+QMenu* MainWindow::createPopupMenu()
{
QMenu* filteredMenu = QMainWindow::createPopupMenu();
- filteredMenu->removeAction( ui->mainToolBar->toggleViewAction() );
+ filteredMenu->removeAction(ui->mainToolBar->toggleViewAction());
filteredMenu->addAction(ui->actionLockToolbars);
@@ -468,22 +458,38 @@ void MainWindow::lockToolbars(bool state)
APPLICATION->settings()->set("ToolbarsLocked", state);
}
-
void MainWindow::konamiTriggered()
{
- qDebug() << "Super Secret Mode ACTIVATED!";
+ QString gradient =
+ " stop:0 rgba(125, 0, 0, 255), stop:0.166 rgba(125, 125, 0, 255), stop:0.333 rgba(0, 125, 0, 255), stop:0.5 rgba(0, 125, 125, "
+ "255), stop:0.666 rgba(0, 0, 125, 255), stop:0.833 rgba(125, 0, 125, 255), stop:1 rgba(125, 0, 0, 255));";
+ QString stylesheet = "background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:0," + gradient;
+ if (ui->mainToolBar->styleSheet() == stylesheet) {
+ ui->mainToolBar->setStyleSheet("");
+ ui->instanceToolBar->setStyleSheet("");
+ ui->centralWidget->setStyleSheet("");
+ ui->newsToolBar->setStyleSheet("");
+ ui->statusBar->setStyleSheet("");
+ qDebug() << "Super Secret Mode DEACTIVATED!";
+ } else {
+ ui->mainToolBar->setStyleSheet(stylesheet);
+ ui->instanceToolBar->setStyleSheet("background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:0, y2:1," + gradient);
+ ui->centralWidget->setStyleSheet("background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:1," + gradient);
+ ui->newsToolBar->setStyleSheet(stylesheet);
+ ui->statusBar->setStyleSheet(stylesheet);
+ qDebug() << "Super Secret Mode ACTIVATED!";
+ }
}
-void MainWindow::showInstanceContextMenu(const QPoint &pos)
+void MainWindow::showInstanceContextMenu(const QPoint& pos)
{
- QList<QAction *> actions;
+ QList<QAction*> actions;
- QAction *actionSep = new QAction("", this);
+ QAction* actionSep = new QAction("", this);
actionSep->setSeparator(true);
bool onInstance = view->indexAt(pos).isValid();
- if (onInstance)
- {
+ if (onInstance) {
// reuse the file menu actions
actions = ui->fileMenu->actions();
@@ -497,21 +503,18 @@ void MainWindow::showInstanceContextMenu(const QPoint &pos)
// add header
actions.prepend(actionSep);
- QAction *actionVoid = new QAction(m_selectedInstance->name(), this);
+ QAction* actionVoid = new QAction(m_selectedInstance->name(), this);
actionVoid->setEnabled(false);
actions.prepend(actionVoid);
- }
- else
- {
+ } else {
auto group = view->groupNameAt(pos);
- QAction *actionVoid = new QAction(BuildConfig.LAUNCHER_DISPLAYNAME, this);
+ QAction* actionVoid = new QAction(BuildConfig.LAUNCHER_DISPLAYNAME, this);
actionVoid->setEnabled(false);
- QAction *actionCreateInstance = new QAction(tr("Create instance"), this);
+ QAction* actionCreateInstance = new QAction(tr("Create instance"), this);
actionCreateInstance->setToolTip(ui->actionAddInstance->toolTip());
- if(!group.isNull())
- {
+ if (!group.isNull()) {
QVariantMap data;
data["group"] = group;
actionCreateInstance->setData(data);
@@ -522,9 +525,8 @@ void MainWindow::showInstanceContextMenu(const QPoint &pos)
actions.prepend(actionSep);
actions.prepend(actionVoid);
actions.append(actionCreateInstance);
- if(!group.isNull())
- {
- QAction *actionDeleteGroup = new QAction(tr("Delete group '%1'").arg(group), this);
+ if (!group.isNull()) {
+ QAction* actionDeleteGroup = new QAction(tr("Delete group '%1'").arg(group), this);
QVariantMap data;
data["group"] = group;
actionDeleteGroup->setData(data);
@@ -555,39 +557,27 @@ void MainWindow::updateToolsMenu()
ui->actionLaunchInstanceOffline->setDisabled(!m_selectedInstance || currentInstanceRunning);
ui->actionLaunchInstanceDemo->setDisabled(!m_selectedInstance || currentInstanceRunning);
- QMenu *launchMenu = ui->actionLaunchInstance->menu();
- if (launchMenu)
- {
+ QMenu* launchMenu = ui->actionLaunchInstance->menu();
+ if (launchMenu) {
launchMenu->clear();
- }
- else
- {
+ } else {
launchMenu = new QMenu(this);
}
- QAction *normalLaunch = launchMenu->addAction(tr("Launch"));
+ QAction* normalLaunch = launchMenu->addAction(tr("Launch"));
normalLaunch->setShortcut(QKeySequence::Open);
- QAction *normalLaunchOffline = launchMenu->addAction(tr("Launch Offline"));
+ QAction* normalLaunchOffline = launchMenu->addAction(tr("Launch Offline"));
normalLaunchOffline->setShortcut(QKeySequence(tr("Ctrl+Shift+O")));
- QAction *normalLaunchDemo = launchMenu->addAction(tr("Launch Demo"));
+ QAction* normalLaunchDemo = launchMenu->addAction(tr("Launch Demo"));
normalLaunchDemo->setShortcut(QKeySequence(tr("Ctrl+Alt+O")));
- if (m_selectedInstance)
- {
+ if (m_selectedInstance) {
normalLaunch->setEnabled(m_selectedInstance->canLaunch());
normalLaunchOffline->setEnabled(m_selectedInstance->canLaunch());
normalLaunchDemo->setEnabled(m_selectedInstance->canLaunch());
- connect(normalLaunch, &QAction::triggered, [this]() {
- APPLICATION->launch(m_selectedInstance, true, false);
- });
- connect(normalLaunchOffline, &QAction::triggered, [this]() {
- APPLICATION->launch(m_selectedInstance, false, false);
- });
- connect(normalLaunchDemo, &QAction::triggered, [this]() {
- APPLICATION->launch(m_selectedInstance, false, true);
- });
- }
- else
- {
+ connect(normalLaunch, &QAction::triggered, [this]() { APPLICATION->launch(m_selectedInstance, true, false); });
+ connect(normalLaunchOffline, &QAction::triggered, [this]() { APPLICATION->launch(m_selectedInstance, false, false); });
+ connect(normalLaunchDemo, &QAction::triggered, [this]() { APPLICATION->launch(m_selectedInstance, false, true); });
+ } else {
normalLaunch->setDisabled(true);
normalLaunchOffline->setDisabled(true);
normalLaunchDemo->setDisabled(true);
@@ -601,35 +591,25 @@ void MainWindow::updateToolsMenu()
QString profilersTitle = tr("Profilers");
launchMenu->addSeparator()->setText(profilersTitle);
- for (auto profiler : APPLICATION->profilers().values())
- {
- QAction *profilerAction = launchMenu->addAction(profiler->name());
- QAction *profilerOfflineAction = launchMenu->addAction(tr("%1 Offline").arg(profiler->name()));
+ for (auto profiler : APPLICATION->profilers().values()) {
+ QAction* profilerAction = launchMenu->addAction(profiler->name());
+ QAction* profilerOfflineAction = launchMenu->addAction(tr("%1 Offline").arg(profiler->name()));
QString error;
- if (!profiler->check(&error))
- {
+ if (!profiler->check(&error)) {
profilerAction->setDisabled(true);
profilerOfflineAction->setDisabled(true);
QString profilerToolTip = tr("Profiler not setup correctly. Go into settings, \"External Tools\".");
profilerAction->setToolTip(profilerToolTip);
profilerOfflineAction->setToolTip(profilerToolTip);
- }
- else if (m_selectedInstance)
- {
+ } else if (m_selectedInstance) {
profilerAction->setEnabled(m_selectedInstance->canLaunch());
profilerOfflineAction->setEnabled(m_selectedInstance->canLaunch());
- connect(profilerAction, &QAction::triggered, [this, profiler]()
- {
- APPLICATION->launch(m_selectedInstance, true, false, profiler.get());
- });
- connect(profilerOfflineAction, &QAction::triggered, [this, profiler]()
- {
- APPLICATION->launch(m_selectedInstance, false, false, profiler.get());
- });
- }
- else
- {
+ connect(profilerAction, &QAction::triggered,
+ [this, profiler]() { APPLICATION->launch(m_selectedInstance, true, false, profiler.get()); });
+ connect(profilerOfflineAction, &QAction::triggered,
+ [this, profiler]() { APPLICATION->launch(m_selectedInstance, false, false, profiler.get()); });
+ } else {
profilerAction->setDisabled(true);
profilerOfflineAction->setDisabled(true);
}
@@ -639,7 +619,7 @@ void MainWindow::updateToolsMenu()
void MainWindow::updateThemeMenu()
{
- QMenu *themeMenu = ui->actionChangeTheme->menu();
+ QMenu* themeMenu = ui->actionChangeTheme->menu();
if (themeMenu) {
themeMenu->clear();
@@ -649,10 +629,10 @@ void MainWindow::updateThemeMenu()
auto themes = APPLICATION->getValidApplicationThemes();
- QActionGroup* themesGroup = new QActionGroup( this );
+ QActionGroup* themesGroup = new QActionGroup(this);
for (auto* theme : themes) {
- QAction * themeAction = themeMenu->addAction(theme->name());
+ QAction* themeAction = themeMenu->addAction(theme->name());
themeAction->setCheckable(true);
if (APPLICATION->settings()->get("ApplicationTheme").toString() == theme->id()) {
@@ -673,51 +653,54 @@ void MainWindow::repopulateAccountsMenu()
{
ui->accountsMenu->clear();
+ // NOTE: this is done so the accounts button text is not set to the accounts menu title
+ QMenu* accountsButtonMenu = ui->actionAccountsButton->menu();
+ if (accountsButtonMenu) {
+ accountsButtonMenu->clear();
+ } else {
+ accountsButtonMenu = new QMenu(this);
+ ui->actionAccountsButton->setMenu(accountsButtonMenu);
+ }
+
auto accounts = APPLICATION->accounts();
MinecraftAccountPtr defaultAccount = accounts->defaultAccount();
QString active_profileId = "";
- if (defaultAccount)
- {
+ if (defaultAccount) {
// this can be called before accountMenuButton exists
- if (ui->actionAccountsButton)
- {
+ if (ui->actionAccountsButton) {
auto profileLabel = profileInUseFilter(defaultAccount->profileName(), defaultAccount->isInUse());
ui->actionAccountsButton->setText(profileLabel);
}
}
- if (accounts->count() <= 0)
- {
+ QActionGroup* accountsGroup = new QActionGroup(this);
+
+ if (accounts->count() <= 0) {
ui->actionNoAccountsAdded->setEnabled(false);
ui->accountsMenu->addAction(ui->actionNoAccountsAdded);
- }
- else
- {
+ } else {
// TODO: Nicer way to iterate?
- for (int i = 0; i < accounts->count(); i++)
- {
+ for (int i = 0; i < accounts->count(); i++) {
MinecraftAccountPtr account = accounts->at(i);
auto profileLabel = profileInUseFilter(account->profileName(), account->isInUse());
- QAction *action = new QAction(profileLabel, this);
+ QAction* action = new QAction(profileLabel, this);
action->setData(i);
action->setCheckable(true);
- if (defaultAccount == account)
- {
+ action->setActionGroup(accountsGroup);
+ if (defaultAccount == account) {
action->setChecked(true);
}
auto face = account->getFace();
- if(!face.isNull()) {
+ if (!face.isNull()) {
action->setIcon(face);
- }
- else {
+ } else {
action->setIcon(APPLICATION->getThemedIcon("noaccount"));
}
const int highestNumberKey = 9;
- if(i<highestNumberKey)
- {
+ if (i < highestNumberKey) {
action->setShortcut(QKeySequence(tr("Ctrl+%1").arg(i + 1)));
}
@@ -730,6 +713,7 @@ void MainWindow::repopulateAccountsMenu()
ui->actionNoDefaultAccount->setData(-1);
ui->actionNoDefaultAccount->setChecked(!defaultAccount);
+ ui->actionNoDefaultAccount->setActionGroup(accountsGroup);
ui->accountsMenu->addAction(ui->actionNoDefaultAccount);
@@ -737,12 +721,13 @@ void MainWindow::repopulateAccountsMenu()
ui->accountsMenu->addSeparator();
ui->accountsMenu->addAction(ui->actionManageAccounts);
+
+ accountsButtonMenu->addActions(ui->accountsMenu->actions());
}
void MainWindow::updatesAllowedChanged(bool allowed)
{
- if(!BuildConfig.UPDATER_ENABLED)
- {
+ if (!BuildConfig.UPDATER_ENABLED) {
return;
}
ui->actionCheckUpdate->setEnabled(allowed);
@@ -753,7 +738,7 @@ void MainWindow::updatesAllowedChanged(bool allowed)
*/
void MainWindow::changeActiveAccount()
{
- QAction *sAction = (QAction *)sender();
+ QAction* sAction = (QAction*)sender();
// Profile's associated Mojang username
if (sAction->data().type() != QVariant::Type::Int)
@@ -762,7 +747,7 @@ void MainWindow::changeActiveAccount()
QVariant data = sAction->data();
bool valid = false;
int index = data.toInt(&valid);
- if(!valid) {
+ if (!valid) {
index = -1;
}
auto accounts = APPLICATION->accounts();
@@ -777,15 +762,13 @@ void MainWindow::defaultAccountChanged()
MinecraftAccountPtr account = APPLICATION->accounts()->defaultAccount();
// FIXME: this needs adjustment for MSA
- if (account && account->profileName() != "")
- {
+ if (account && account->profileName() != "") {
auto profileLabel = profileInUseFilter(account->profileName(), account->isInUse());
ui->actionAccountsButton->setText(profileLabel);
auto face = account->getFace();
- if(face.isNull()) {
+ if (face.isNull()) {
ui->actionAccountsButton->setIcon(APPLICATION->getThemedIcon("noaccount"));
- }
- else {
+ } else {
ui->actionAccountsButton->setIcon(face);
}
return;
@@ -796,33 +779,30 @@ void MainWindow::defaultAccountChanged()
ui->actionAccountsButton->setText(tr("Accounts"));
}
-bool MainWindow::eventFilter(QObject *obj, QEvent *ev)
+bool MainWindow::eventFilter(QObject* obj, QEvent* ev)
{
- if (obj == view)
- {
- if (ev->type() == QEvent::KeyPress)
- {
+ if (obj == view) {
+ if (ev->type() == QEvent::KeyPress) {
secretEventFilter->input(ev);
- QKeyEvent *keyEvent = static_cast<QKeyEvent *>(ev);
- switch (keyEvent->key())
- {
- /*
- case Qt::Key_Enter:
- case Qt::Key_Return:
- activateInstance(m_selectedInstance);
- return true;
- */
- case Qt::Key_Delete:
- on_actionDeleteInstance_triggered();
- return true;
- case Qt::Key_F5:
- refreshInstances();
- return true;
- case Qt::Key_F2:
- on_actionRenameInstance_triggered();
- return true;
- default:
- break;
+ QKeyEvent* keyEvent = static_cast<QKeyEvent*>(ev);
+ switch (keyEvent->key()) {
+ /*
+ case Qt::Key_Enter:
+ case Qt::Key_Return:
+ activateInstance(m_selectedInstance);
+ return true;
+ */
+ case Qt::Key_Delete:
+ on_actionDeleteInstance_triggered();
+ return true;
+ case Qt::Key_F5:
+ refreshInstances();
+ return true;
+ case Qt::Key_F2:
+ on_actionRenameInstance_triggered();
+ return true;
+ default:
+ break;
}
}
}
@@ -831,23 +811,17 @@ bool MainWindow::eventFilter(QObject *obj, QEvent *ev)
void MainWindow::updateNewsLabel()
{
- if (m_newsChecker->isLoadingNews())
- {
+ if (m_newsChecker->isLoadingNews()) {
newsLabel->setText(tr("Loading news..."));
newsLabel->setEnabled(false);
ui->actionMoreNews->setVisible(false);
- }
- else
- {
+ } else {
QList<NewsEntryPtr> entries = m_newsChecker->getNewsEntries();
- if (entries.length() > 0)
- {
+ if (entries.length() > 0) {
newsLabel->setText(entries[0]->title);
newsLabel->setEnabled(true);
ui->actionMoreNews->setVisible(true);
- }
- else
- {
+ } else {
newsLabel->setText(tr("No news available."));
newsLabel->setEnabled(false);
ui->actionMoreNews->setVisible(false);
@@ -855,7 +829,7 @@ void MainWindow::updateNewsLabel()
}
}
-QList<int> stringToIntList(const QString &string)
+QList<int> stringToIntList(const QString& string)
{
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
QStringList split = string.split(',', Qt::SkipEmptyParts);
@@ -863,17 +837,15 @@ QList<int> stringToIntList(const QString &string)
QStringList split = string.split(',', QString::SkipEmptyParts);
#endif
QList<int> out;
- for (int i = 0; i < split.size(); ++i)
- {
+ for (int i = 0; i < split.size(); ++i) {
out.append(split.at(i).toInt());
}
return out;
}
-QString intListToString(const QList<int> &list)
+QString intListToString(const QList<int>& list)
{
QStringList slist;
- for (int i = 0; i < list.size(); ++i)
- {
+ for (int i = 0; i < list.size(); ++i) {
slist.append(QString::number(list.at(i)));
}
return slist.join(',');
@@ -887,47 +859,30 @@ void MainWindow::onCatToggled(bool state)
void MainWindow::setCatBackground(bool enabled)
{
- if (enabled) {
- view->setStyleSheet(QString(R"(
-InstanceView
-{
- background-image: url(:/backgrounds/%1);
- background-attachment: fixed;
- background-clip: padding;
- background-position: bottom right;
- background-repeat: none;
- background-color:palette(base);
-})")
- .arg(ThemeManager::getCatImage()));
- } else {
- view->setStyleSheet(QString());
- }
+ view->setPaintCat(enabled);
+ view->viewport()->repaint();
}
-void MainWindow::runModalTask(Task *task)
+void MainWindow::runModalTask(Task* task)
{
- connect(task, &Task::failed, [this](QString reason)
- {
- CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show();
- });
- connect(task, &Task::succeeded, [this, task]()
- {
- QStringList warnings = task->warnings();
- if(warnings.count())
- {
- CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show();
- }
- });
- connect(task, &Task::aborted, [this]
- {
- CustomMessageBox::selectable(this, tr("Task aborted"), tr("The task has been aborted by the user."), QMessageBox::Information)->show();
- });
+ connect(task, &Task::failed,
+ [this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); });
+ connect(task, &Task::succeeded, [this, task]() {
+ QStringList warnings = task->warnings();
+ if (warnings.count()) {
+ CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show();
+ }
+ });
+ connect(task, &Task::aborted, [this] {
+ CustomMessageBox::selectable(this, tr("Task aborted"), tr("The task has been aborted by the user."), QMessageBox::Information)
+ ->show();
+ });
ProgressDialog loadDialog(this);
loadDialog.setSkipButton(true, tr("Abort"));
loadDialog.execWithTask(task);
}
-void MainWindow::instanceFromInstanceTask(InstanceTask *rawTask)
+void MainWindow::instanceFromInstanceTask(InstanceTask* rawTask)
{
unique_qobject_ptr<Task> task(APPLICATION->instances()->wrapInstanceTask(rawTask));
runModalTask(task.get());
@@ -954,52 +909,43 @@ void MainWindow::finalizeInstance(InstancePtr inst)
{
view->updateGeometries();
setSelectedInstanceById(inst->id());
- if (APPLICATION->accounts()->anyAccountIsValid())
- {
+ if (APPLICATION->accounts()->anyAccountIsValid()) {
ProgressDialog loadDialog(this);
auto update = inst->createUpdateTask(Net::Mode::Online);
- connect(update.get(), &Task::failed, [this](QString reason)
- {
- QString error = QString("Instance load failed: %1").arg(reason);
- CustomMessageBox::selectable(this, tr("Error"), error, QMessageBox::Warning)->show();
- });
- if(update)
- {
+ connect(update.get(), &Task::failed, [this](QString reason) {
+ QString error = QString("Instance load failed: %1").arg(reason);
+ CustomMessageBox::selectable(this, tr("Error"), error, QMessageBox::Warning)->show();
+ });
+ if (update) {
loadDialog.setSkipButton(true, tr("Abort"));
loadDialog.execWithTask(update.get());
}
- }
- else
- {
- CustomMessageBox::selectable(
- this,
- tr("Error"),
- tr("The launcher cannot download Minecraft or update instances unless you have at least "
- "one account added.\nPlease add your Mojang or Minecraft account."),
- QMessageBox::Warning
- )->show();
+ } else {
+ CustomMessageBox::selectable(this, tr("Error"),
+ tr("The launcher cannot download Minecraft or update instances unless you have at least "
+ "one account added.\nPlease add your Mojang or Minecraft account."),
+ QMessageBox::Warning)
+ ->show();
}
}
void MainWindow::addInstance(QString url)
{
QString groupName;
- do
- {
+ do {
QObject* obj = sender();
- if(!obj)
+ if (!obj)
break;
- QAction *action = qobject_cast<QAction *>(obj);
- if(!action)
+ QAction* action = qobject_cast<QAction*>(obj);
+ if (!action)
break;
auto map = action->data().toMap();
- if(!map.contains("group"))
+ if (!map.contains("group"))
break;
groupName = map["group"].toString();
- } while(0);
+ } while (0);
- if(groupName.isEmpty())
- {
+ if (groupName.isEmpty()) {
groupName = APPLICATION->settings()->get("LastUsedGroupForNewInstance").toString();
}
@@ -1009,9 +955,8 @@ void MainWindow::addInstance(QString url)
APPLICATION->settings()->set("LastUsedGroupForNewInstance", newInstDlg.instGroup());
- InstanceTask * creationTask = newInstDlg.extractTask();
- if(creationTask)
- {
+ InstanceTask* creationTask = newInstDlg.extractTask();
+ if (creationTask) {
instanceFromInstanceTask(creationTask);
}
}
@@ -1036,7 +981,7 @@ void MainWindow::processURLs(QList<QUrl> urls)
break;
}
- auto localFileName = QDir::toNativeSeparators(url.toLocalFile()) ;
+ auto localFileName = QDir::toNativeSeparators(url.toLocalFile());
QFileInfo localFileInfo(localFileName);
auto type = ResourceUtils::identify(localFileInfo);
@@ -1105,8 +1050,7 @@ void MainWindow::on_actionChangeInstIcon_triggered()
IconPickerDialog dlg(this);
dlg.execWithSelection(m_selectedInstance->iconKey());
- if (dlg.result() == QDialog::Accepted)
- {
+ if (dlg.result() == QDialog::Accepted) {
m_selectedInstance->setIconKey(dlg.selectedIconKey);
auto icon = APPLICATION->icons()->getIcon(dlg.selectedIconKey);
ui->actionChangeInstIcon->setIcon(icon);
@@ -1116,8 +1060,7 @@ void MainWindow::on_actionChangeInstIcon_triggered()
void MainWindow::iconUpdated(QString icon)
{
- if (icon == m_currentInstIcon)
- {
+ if (icon == m_currentInstIcon) {
auto icon = APPLICATION->icons()->getIcon(m_currentInstIcon);
ui->actionChangeInstIcon->setIcon(icon);
changeIconButton->setIcon(icon);
@@ -1132,13 +1075,12 @@ void MainWindow::updateInstanceToolIcon(QString new_icon)
changeIconButton->setIcon(icon);
}
-void MainWindow::setSelectedInstanceById(const QString &id)
+void MainWindow::setSelectedInstanceById(const QString& id)
{
if (id.isNull())
return;
const QModelIndex index = APPLICATION->instances()->getInstanceIndexById(id);
- if (index.isValid())
- {
+ if (index.isValid()) {
QModelIndex selectionIndex = proxymodel->mapFromSource(index);
view->selectionModel()->setCurrentIndex(selectionIndex, QItemSelectionModel::ClearAndSelect);
updateStatusCenter();
@@ -1160,8 +1102,7 @@ void MainWindow::on_actionChangeInstGroup_triggered()
name = QInputDialog::getItem(this, tr("Group name"), tr("Enter a new group name."), groups, foo, true, &ok);
name = name.simplified();
- if (ok)
- {
+ if (ok) {
APPLICATION->instances()->setInstanceGroup(instId, name);
}
}
@@ -1169,21 +1110,19 @@ void MainWindow::on_actionChangeInstGroup_triggered()
void MainWindow::deleteGroup()
{
QObject* obj = sender();
- if(!obj)
+ if (!obj)
return;
- QAction *action = qobject_cast<QAction *>(obj);
- if(!action)
+ QAction* action = qobject_cast<QAction*>(obj);
+ if (!action)
return;
auto map = action->data().toMap();
- if(!map.contains("group"))
+ if (!map.contains("group"))
return;
QString groupName = map["group"].toString();
- if(!groupName.isEmpty())
- {
- auto reply = QMessageBox::question(this, tr("Delete group"), tr("Are you sure you want to delete the group %1?")
- .arg(groupName), QMessageBox::Yes | QMessageBox::No);
- if(reply == QMessageBox::Yes)
- {
+ if (!groupName.isEmpty()) {
+ auto reply = QMessageBox::question(this, tr("Delete group"), tr("Are you sure you want to delete the group %1?").arg(groupName),
+ QMessageBox::Yes | QMessageBox::No);
+ if (reply == QMessageBox::Yes) {
APPLICATION->instances()->deleteGroup(groupName);
}
}
@@ -1201,6 +1140,12 @@ void MainWindow::on_actionViewInstanceFolder_triggered()
DesktopServices::openDirectory(str);
}
+void MainWindow::on_actionViewLauncherRootFolder_triggered()
+{
+ const QString dataPath = QDir::currentPath();
+ DesktopServices::openDirectory(dataPath);
+}
+
void MainWindow::refreshInstances()
{
APPLICATION->instances()->loadList();
@@ -1213,12 +1158,9 @@ void MainWindow::on_actionViewCentralModsFolder_triggered()
void MainWindow::checkForUpdates()
{
- if(BuildConfig.UPDATER_ENABLED)
- {
+ if (BuildConfig.UPDATER_ENABLED) {
APPLICATION->triggerUpdateCheck();
- }
- else
- {
+ } else {
qWarning() << "Updater not set up. Cannot check for updates.";
}
}
@@ -1246,7 +1188,17 @@ void MainWindow::globalSettingsClosed()
void MainWindow::on_actionEditInstance_triggered()
{
- APPLICATION->showInstanceWindow(m_selectedInstance);
+ if (!m_selectedInstance)
+ return;
+
+ if (m_selectedInstance->canEdit()) {
+ APPLICATION->showInstanceWindow(m_selectedInstance);
+ } else {
+ CustomMessageBox::selectable(this, tr("Instance not editable"),
+ tr("This instance is not editable. It may be broken, invalid, or too old. Check logs for details."),
+ QMessageBox::Critical)
+ ->show();
+ }
}
void MainWindow::on_actionManageAccounts_triggered()
@@ -1308,7 +1260,8 @@ void MainWindow::newsButtonClicked()
news_dialog.exec();
}
-void MainWindow::onCatChanged(int) {
+void MainWindow::onCatChanged(int)
+{
setCatBackground(APPLICATION->settings()->get("TheCat").toBool());
}
@@ -1337,6 +1290,21 @@ void MainWindow::on_actionDeleteInstance_triggered()
if (response != QMessageBox::Yes)
return;
+ auto linkedInstances = APPLICATION->instances()->getLinkedInstancesById(id);
+ if (!linkedInstances.empty()) {
+ response = CustomMessageBox::selectable(this, tr("There are linked instances"),
+ tr("The following instance(s) might reference files in this instance:\n\n"
+ "%1\n\n"
+ "Deleting it could break the other instance(s), \n\n"
+ "Do you wish to proceed?",
+ nullptr, linkedInstances.count())
+ .arg(linkedInstances.join("\n")),
+ QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No)
+ ->exec();
+ if (response != QMessageBox::Yes)
+ return;
+ }
+
if (APPLICATION->instances()->trashInstance(id)) {
ui->actionUndoTrashInstance->setEnabled(APPLICATION->instances()->trashedSomething());
return;
@@ -1345,33 +1313,70 @@ void MainWindow::on_actionDeleteInstance_triggered()
APPLICATION->instances()->deleteInstance(id);
}
-void MainWindow::on_actionExportInstance_triggered()
+void MainWindow::on_actionExportInstanceZip_triggered()
{
- if (m_selectedInstance)
- {
+ if (m_selectedInstance) {
ExportInstanceDialog dlg(m_selectedInstance, this);
dlg.exec();
}
}
+void MainWindow::on_actionExportInstanceMrPack_triggered()
+{
+ if (m_selectedInstance) {
+ ExportPackDialog dlg(m_selectedInstance, this);
+ dlg.exec();
+ }
+}
+
+void MainWindow::on_actionExportInstanceToModList_triggered()
+{
+ if (m_selectedInstance) {
+ ExportToModListDialog dlg(m_selectedInstance, this);
+ dlg.exec();
+ }
+}
+
+void MainWindow::on_actionExportInstanceFlamePack_triggered()
+{
+ if (m_selectedInstance) {
+ auto instance = dynamic_cast<MinecraftInstance*>(m_selectedInstance.get());
+ if (instance) {
+ QString errorMsg;
+ if (instance->getPackProfile()->getComponent("org.quiltmc.quilt-loader")) {
+ errorMsg = tr("Quilt is currently not supported by CurseForge modpacks.");
+ } else if (auto cmp = instance->getPackProfile()->getComponent("net.minecraft");
+ cmp && cmp->getVersionFile() && cmp->getVersionFile()->type == "snapshot") {
+ errorMsg = tr("Snapshots are currently not supported by CurseForge modpacks.");
+ }
+ if (!errorMsg.isEmpty()) {
+ QMessageBox msgBox;
+ msgBox.setText(errorMsg);
+ msgBox.exec();
+ return;
+ }
+ ExportPackDialog dlg(m_selectedInstance, this, ModPlatform::ResourceProvider::FLAME);
+ dlg.exec();
+ }
+ }
+}
+
void MainWindow::on_actionRenameInstance_triggered()
{
- if (m_selectedInstance)
- {
+ if (m_selectedInstance) {
view->edit(view->currentIndex());
}
}
void MainWindow::on_actionViewSelectedInstFolder_triggered()
{
- if (m_selectedInstance)
- {
+ if (m_selectedInstance) {
QString str = m_selectedInstance->instanceRoot();
DesktopServices::openDirectory(QDir(str).absolutePath());
}
}
-void MainWindow::closeEvent(QCloseEvent *event)
+void MainWindow::closeEvent(QCloseEvent* event)
{
// Save the window state and geometry.
APPLICATION->settings()->set("MainWindowState", saveState().toBase64());
@@ -1383,8 +1388,7 @@ void MainWindow::closeEvent(QCloseEvent *event)
void MainWindow::changeEvent(QEvent* event)
{
- if (event->type() == QEvent::LanguageChange)
- {
+ if (event->type() == QEvent::LanguageChange) {
retranslateUi();
}
QMainWindow::changeEvent(event);
@@ -1404,8 +1408,7 @@ void MainWindow::instanceActivated(QModelIndex index)
void MainWindow::on_actionLaunchInstance_triggered()
{
- if(m_selectedInstance && !m_selectedInstance->isRunning())
- {
+ if (m_selectedInstance && !m_selectedInstance->isRunning()) {
APPLICATION->launch(m_selectedInstance);
}
}
@@ -1417,187 +1420,185 @@ void MainWindow::activateInstance(InstancePtr instance)
void MainWindow::on_actionLaunchInstanceOffline_triggered()
{
- if (m_selectedInstance)
- {
+ if (m_selectedInstance) {
APPLICATION->launch(m_selectedInstance, false);
}
}
void MainWindow::on_actionLaunchInstanceDemo_triggered()
{
- if (m_selectedInstance)
- {
+ if (m_selectedInstance) {
APPLICATION->launch(m_selectedInstance, false, true);
}
}
void MainWindow::on_actionKillInstance_triggered()
{
- if(m_selectedInstance && m_selectedInstance->isRunning())
- {
+ if (m_selectedInstance && m_selectedInstance->isRunning()) {
APPLICATION->kill(m_selectedInstance);
}
}
void MainWindow::on_actionCreateInstanceShortcut_triggered()
{
- if (m_selectedInstance)
- {
- auto desktopPath = FS::getDesktopDir();
- if (desktopPath.isEmpty()) {
- // TODO come up with an alternative solution (open "save file" dialog)
- QMessageBox::critical(this, tr("Create instance shortcut"), tr("Couldn't find desktop?!"));
- return;
- }
+ if (!m_selectedInstance)
+ return;
+ auto desktopPath = FS::getDesktopDir();
+ if (desktopPath.isEmpty()) {
+ // TODO come up with an alternative solution (open "save file" dialog)
+ QMessageBox::critical(this, tr("Create instance shortcut"), tr("Couldn't find desktop?!"));
+ return;
+ }
+ QString desktopFilePath;
+ QString appPath = QApplication::applicationFilePath();
+ QString iconPath;
+ QStringList args;
#if defined(Q_OS_MACOS)
- QString appPath = QApplication::applicationFilePath();
- if (appPath.startsWith("/private/var/")) {
- QMessageBox::critical(this, tr("Create instance shortcut"), tr("The launcher is in the folder it was extracted from, therefore it cannot create shortcuts."));
- return;
- }
+ appPath = QApplication::applicationFilePath();
+ if (appPath.startsWith("/private/var/")) {
+ QMessageBox::critical(this, tr("Create instance shortcut"),
+ tr("The launcher is in the folder it was extracted from, therefore it cannot create shortcuts."));
+ return;
+ }
- if (FS::createShortcut(FS::PathCombine(desktopPath, m_selectedInstance->name()),
- appPath, { "--launch", m_selectedInstance->id() },
- m_selectedInstance->name(), "")) {
- QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance on your desktop!"));
- }
- else
- {
- QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create instance shortcut!"));
- }
+ auto pIcon = APPLICATION->icons()->icon(m_selectedInstance->iconKey());
+ if (pIcon == nullptr) {
+ pIcon = APPLICATION->icons()->icon("grass");
+ }
+
+ iconPath = FS::PathCombine(m_selectedInstance->instanceRoot(), "Icon.icns");
+
+ QFile iconFile(iconPath);
+ if (!iconFile.open(QFile::WriteOnly)) {
+ QMessageBox::critical(this, tr("Create instance Application"), tr("Failed to create icon for Application."));
+ return;
+ }
+
+ QIcon icon = pIcon->icon();
+
+ bool success = icon.pixmap(1024, 1024).save(iconPath, "ICNS");
+ iconFile.close();
+
+ if (!success) {
+ iconFile.remove();
+ QMessageBox::critical(this, tr("Create instance Application"), tr("Failed to create icon for Application."));
+ return;
+ }
#elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
- QString appPath = QApplication::applicationFilePath();
- if (appPath.startsWith("/tmp/.mount_")) {
- // AppImage!
- appPath = QProcessEnvironment::systemEnvironment().value(QStringLiteral("APPIMAGE"));
- if (appPath.isEmpty())
- {
- QMessageBox::critical(this, tr("Create instance shortcut"), tr("Launcher is running as misconfigured AppImage? ($APPIMAGE environment variable is missing)"));
- }
- else if (appPath.endsWith("/"))
- {
- appPath.chop(1);
- }
+ if (appPath.startsWith("/tmp/.mount_")) {
+ // AppImage!
+ appPath = QProcessEnvironment::systemEnvironment().value(QStringLiteral("APPIMAGE"));
+ if (appPath.isEmpty()) {
+ QMessageBox::critical(this, tr("Create instance shortcut"),
+ tr("Launcher is running as misconfigured AppImage? ($APPIMAGE environment variable is missing)"));
+ } else if (appPath.endsWith("/")) {
+ appPath.chop(1);
}
+ }
- auto icon = APPLICATION->icons()->icon(m_selectedInstance->iconKey());
- if (icon == nullptr)
- {
- icon = APPLICATION->icons()->icon("grass");
- }
+ auto icon = APPLICATION->icons()->icon(m_selectedInstance->iconKey());
+ if (icon == nullptr) {
+ icon = APPLICATION->icons()->icon("grass");
+ }
- QString iconPath = FS::PathCombine(m_selectedInstance->instanceRoot(), "icon.png");
+ iconPath = FS::PathCombine(m_selectedInstance->instanceRoot(), "icon.png");
- QFile iconFile(iconPath);
- if (!iconFile.open(QFile::WriteOnly))
- {
- QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut."));
- return;
- }
- bool success = icon->icon().pixmap(64, 64).save(&iconFile, "PNG");
- iconFile.close();
+ QFile iconFile(iconPath);
+ if (!iconFile.open(QFile::WriteOnly)) {
+ QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut."));
+ return;
+ }
+ bool success = icon->icon().pixmap(64, 64).save(&iconFile, "PNG");
+ iconFile.close();
- if (!success)
- {
- iconFile.remove();
- QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut."));
- return;
- }
+ if (!success) {
+ iconFile.remove();
+ QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut."));
+ return;
+ }
+
+ if (DesktopServices::isFlatpak()) {
+ desktopFilePath = FS::PathCombine(desktopPath, FS::RemoveInvalidFilenameChars(m_selectedInstance->name()) + ".desktop");
+ QFileDialog fileDialog;
+ // workaround to make sure the portal file dialog opens in the desktop directory
+ fileDialog.setDirectoryUrl(desktopPath);
+ desktopFilePath = fileDialog.getSaveFileName(this, tr("Create Shortcut"), desktopFilePath, tr("Desktop Entries (*.desktop)"));
+ if (desktopFilePath.isEmpty())
+ return; // file dialog canceled by user
+ appPath = "flatpak";
+ QString flatpakAppId = BuildConfig.LAUNCHER_DESKTOPFILENAME;
+ flatpakAppId.remove(".desktop");
+ args.append({ "run", flatpakAppId });
+ }
- QString desktopFilePath = FS::PathCombine(desktopPath, m_selectedInstance->name() + ".desktop");
- QStringList args;
- if (DesktopServices::isFlatpak()) {
- QFileDialog fileDialog;
- // workaround to make sure the portal file dialog opens in the desktop directory
- fileDialog.setDirectoryUrl(desktopPath);
- desktopFilePath = fileDialog.getSaveFileName(
- this, tr("Create Shortcut"), desktopFilePath,
- tr("Desktop Entries (*.desktop)"));
- if (desktopFilePath.isEmpty())
- return; // file dialog canceled by user
- appPath = "flatpak";
- QString flatpakAppId = BuildConfig.LAUNCHER_DESKTOPFILENAME;
- flatpakAppId.remove(".desktop");
- args.append({ "run", flatpakAppId });
- }
- args.append({ "--launch", m_selectedInstance->id() });
- if (FS::createShortcut(desktopFilePath, appPath, args, m_selectedInstance->name(), iconPath)) {
- QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance on your desktop!"));
- }
- else
- {
- iconFile.remove();
- QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create instance shortcut!"));
- }
#elif defined(Q_OS_WIN)
- auto icon = APPLICATION->icons()->icon(m_selectedInstance->iconKey());
- if (icon == nullptr)
- {
- icon = APPLICATION->icons()->icon("grass");
- }
+ auto icon = APPLICATION->icons()->icon(m_selectedInstance->iconKey());
+ if (icon == nullptr) {
+ icon = APPLICATION->icons()->icon("grass");
+ }
- QString iconPath = FS::PathCombine(m_selectedInstance->instanceRoot(), "icon.ico");
+ iconPath = FS::PathCombine(m_selectedInstance->instanceRoot(), "icon.ico");
- // part of fix for weird bug involving the window icon being replaced
- // dunno why it happens, but this 2-line fix seems to be enough, so w/e
- auto appIcon = APPLICATION->getThemedIcon("logo");
+ // part of fix for weird bug involving the window icon being replaced
+ // dunno why it happens, but this 2-line fix seems to be enough, so w/e
+ auto appIcon = APPLICATION->getThemedIcon("logo");
- QFile iconFile(iconPath);
- if (!iconFile.open(QFile::WriteOnly))
- {
- QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut."));
- return;
- }
- bool success = icon->icon().pixmap(64, 64).save(&iconFile, "ICO");
- iconFile.close();
+ QFile iconFile(iconPath);
+ if (!iconFile.open(QFile::WriteOnly)) {
+ QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut."));
+ return;
+ }
+ bool success = icon->icon().pixmap(64, 64).save(&iconFile, "ICO");
+ iconFile.close();
- // restore original window icon
- QGuiApplication::setWindowIcon(appIcon);
+ // restore original window icon
+ QGuiApplication::setWindowIcon(appIcon);
- if (!success)
- {
- iconFile.remove();
- QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut."));
- return;
- }
+ if (!success) {
+ iconFile.remove();
+ QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut."));
+ return;
+ }
- if (FS::createShortcut(FS::PathCombine(desktopPath, m_selectedInstance->name()),
- QApplication::applicationFilePath(), { "--launch", m_selectedInstance->id() },
- m_selectedInstance->name(), iconPath)) {
- QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance on your desktop!"));
- }
- else
- {
- iconFile.remove();
- QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create instance shortcut!"));
- }
#else
- QMessageBox::critical(this, tr("Create instance shortcut"), tr("Not supported on your platform!"));
+ QMessageBox::critical(this, tr("Create instance shortcut"), tr("Not supported on your platform!"));
+ return;
+#endif
+ args.append({ "--launch", m_selectedInstance->id() });
+ if (FS::createShortcut(desktopFilePath, appPath, args, m_selectedInstance->name(), iconPath)) {
+#if not defined(Q_OS_MACOS)
+ QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance on your desktop!"));
+#else
+ QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance!"));
+#endif
+ } else {
+#if not defined(Q_OS_MACOS)
+ iconFile.remove();
#endif
+ QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create instance shortcut!"));
}
}
void MainWindow::taskEnd()
{
- QObject *sender = QObject::sender();
+ QObject* sender = QObject::sender();
if (sender == m_versionLoadTask)
m_versionLoadTask = NULL;
sender->deleteLater();
}
-void MainWindow::startTask(Task *task)
+void MainWindow::startTask(Task* task)
{
connect(task, SIGNAL(succeeded()), SLOT(taskEnd()));
connect(task, SIGNAL(failed(QString)), SLOT(taskEnd()));
task->start();
}
-void MainWindow::instanceChanged(const QModelIndex &current, const QModelIndex &previous)
+void MainWindow::instanceChanged(const QModelIndex& current, const QModelIndex& previous)
{
- if (!current.isValid())
- {
+ if (!current.isValid()) {
APPLICATION->settings()->set("SelectedInstance", QString());
selectionBad();
return;
@@ -1607,8 +1608,7 @@ void MainWindow::instanceChanged(const QModelIndex &current, const QModelIndex &
}
QString id = current.data(InstanceList::InstanceIDRole).toString();
m_selectedInstance = APPLICATION->instances()->getInstanceById(id);
- if (m_selectedInstance)
- {
+ if (m_selectedInstance) {
ui->instanceToolBar->setEnabled(true);
setInstanceActionsEnabled(true);
ui->actionLaunchInstance->setEnabled(m_selectedInstance->canLaunch());
@@ -1618,7 +1618,7 @@ void MainWindow::instanceChanged(const QModelIndex &current, const QModelIndex &
// Disable demo-mode if not available.
auto instance = dynamic_cast<MinecraftInstance*>(m_selectedInstance.get());
if (instance) {
- ui->actionLaunchInstanceDemo->setEnabled(instance->supportsDemo());
+ ui->actionLaunchInstanceDemo->setEnabled(instance->supportsDemo());
}
ui->actionKillInstance->setEnabled(m_selectedInstance->isRunning());
@@ -1633,9 +1633,7 @@ void MainWindow::instanceChanged(const QModelIndex &current, const QModelIndex &
APPLICATION->settings()->set("SelectedInstance", m_selectedInstance->id());
connect(m_selectedInstance.get(), &BaseInstance::runningStatusChanged, this, &MainWindow::refreshCurrentInstance);
- }
- else
- {
+ } else {
ui->instanceToolBar->setEnabled(false);
setInstanceActionsEnabled(false);
ui->actionLaunchInstance->setEnabled(false);
@@ -1653,12 +1651,11 @@ void MainWindow::instanceSelectRequest(QString id)
setSelectedInstanceById(id);
}
-void MainWindow::instanceDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight)
+void MainWindow::instanceDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight)
{
auto current = view->selectionModel()->currentIndex();
QItemSelection test(topLeft, bottomRight);
- if (test.contains(current))
- {
+ if (test.contains(current)) {
instanceChanged(current, current);
}
}
@@ -1682,34 +1679,28 @@ void MainWindow::selectionBad()
void MainWindow::checkInstancePathForProblems()
{
QString instanceFolder = APPLICATION->settings()->get("InstanceDir").toString();
- if (FS::checkProblemticPathJava(QDir(instanceFolder)))
- {
+ if (FS::checkProblemticPathJava(QDir(instanceFolder))) {
QMessageBox warning(this);
warning.setText(tr("Your instance folder contains \'!\' and this is known to cause Java problems!"));
- warning.setInformativeText(
- tr(
- "You have now two options: <br/>"
- " - change the instance folder in the settings <br/>"
- " - move this installation of %1 to a different folder"
- ).arg(BuildConfig.LAUNCHER_DISPLAYNAME)
- );
+ warning.setInformativeText(tr("You have now two options: <br/>"
+ " - change the instance folder in the settings <br/>"
+ " - move this installation of %1 to a different folder")
+ .arg(BuildConfig.LAUNCHER_DISPLAYNAME));
warning.setDefaultButton(QMessageBox::Ok);
warning.exec();
}
- auto tempFolderText = tr("This is a problem: <br/>"
- " - The launcher will likely be deleted without warning by the operating system <br/>"
- " - close the launcher now and extract it to a real location, not a temporary folder");
+ auto tempFolderText =
+ tr("This is a problem: <br/>"
+ " - The launcher will likely be deleted without warning by the operating system <br/>"
+ " - close the launcher now and extract it to a real location, not a temporary folder");
QString pathfoldername = QDir(instanceFolder).absolutePath();
- if (pathfoldername.contains("Rar$", Qt::CaseInsensitive))
- {
+ if (pathfoldername.contains("Rar$", Qt::CaseInsensitive)) {
QMessageBox warning(this);
warning.setText(tr("Your instance folder contains \'Rar$\' - that means you haven't extracted the launcher archive!"));
warning.setInformativeText(tempFolderText);
warning.setDefaultButton(QMessageBox::Ok);
warning.exec();
- }
- else if (pathfoldername.startsWith(QDir::tempPath()) || pathfoldername.contains("/TempState/"))
- {
+ } else if (pathfoldername.startsWith(QDir::tempPath()) || pathfoldername.contains("/TempState/")) {
QMessageBox warning(this);
warning.setText(tr("Your instance folder is in a temporary folder: \'%1\'!").arg(QDir::tempPath()));
warning.setInformativeText(tempFolderText);
diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h
index 3a42c34e..27c2756f 100644
--- a/launcher/ui/MainWindow.h
+++ b/launcher/ui/MainWindow.h
@@ -1,7 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
- * PolyMC - Minecraft Launcher
+ * Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ * Copyright (C) 2023 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
@@ -112,6 +113,8 @@ private slots:
void on_actionViewInstanceFolder_triggered();
+ void on_actionViewLauncherRootFolder_triggered();
+
void on_actionViewSelectedInstFolder_triggered();
void refreshInstances();
@@ -151,7 +154,11 @@ private slots:
void deleteGroup();
void undoTrashInstance();
- void on_actionExportInstance_triggered();
+ inline void on_actionExportInstance_triggered() { on_actionExportInstanceZip_triggered(); }
+ void on_actionExportInstanceZip_triggered();
+ void on_actionExportInstanceMrPack_triggered();
+ void on_actionExportInstanceFlamePack_triggered();
+ void on_actionExportInstanceToModList_triggered();
void on_actionRenameInstance_triggered();
diff --git a/launcher/ui/MainWindow.ui b/launcher/ui/MainWindow.ui
index 2b6a10b1..e4421d40 100644
--- a/launcher/ui/MainWindow.ui
+++ b/launcher/ui/MainWindow.ui
@@ -187,6 +187,7 @@
<bool>true</bool>
</property>
<addaction name="actionViewInstanceFolder"/>
+ <addaction name="actionViewLauncherRootFolder"/>
<addaction name="actionViewCentralModsFolder"/>
</widget>
<widget class="QMenu" name="accountsMenu">
@@ -459,10 +460,39 @@
<string>E&amp;xport...</string>
</property>
<property name="toolTip">
- <string>Export the selected instance as a zip file.</string>
+ <string>Export the selected instance to supported formats.</string>
</property>
- <property name="shortcut">
- <string>Ctrl+E</string>
+ </action>
+ <action name="actionExportInstanceZip">
+ <property name="icon">
+ <iconset theme="launcher"/>
+ </property>
+ <property name="text">
+ <string>Prism Launcher (zip)</string>
+ </property>
+ </action>
+ <action name="actionExportInstanceMrPack">
+ <property name="icon">
+ <iconset theme="modrinth"/>
+ </property>
+ <property name="text">
+ <string>Modrinth (mrpack)</string>
+ </property>
+ </action>
+ <action name="actionExportInstanceFlamePack">
+ <property name="icon">
+ <iconset theme="flame"/>
+ </property>
+ <property name="text">
+ <string>CurseForge (zip)</string>
+ </property>
+ </action>
+ <action name="actionExportInstanceToModList">
+ <property name="icon">
+ <iconset theme="new"/>
+ </property>
+ <property name="text">
+ <string>Mod List</string>
</property>
</action>
<action name="actionCreateInstanceShortcut">
@@ -528,6 +558,18 @@
<string>Open the instance folder in a file browser.</string>
</property>
</action>
+ <action name="actionViewLauncherRootFolder">
+ <property name="icon">
+ <iconset theme="viewfolder">
+ <normaloff>.</normaloff>.</iconset>
+ </property>
+ <property name="text">
+ <string>&amp;View Launcher Root Folder</string>
+ </property>
+ <property name="toolTip">
+ <string>Open the launcher's root folder in a file browser.</string>
+ </property>
+ </action>
<action name="actionViewCentralModsFolder">
<property name="icon">
<iconset theme="centralmods">
@@ -551,7 +593,7 @@
<normaloff>.</normaloff>.</iconset>
</property>
<property name="text">
- <string>Report a &amp;Bug...</string>
+ <string>Report a Bug or Suggest a Feature</string>
</property>
<property name="toolTip">
<string>Open the bug tracker to report a bug with %1.</string>
diff --git a/launcher/ui/dialogs/AboutDialog.cpp b/launcher/ui/dialogs/AboutDialog.cpp
index 76e3d8ed..88739463 100644
--- a/launcher/ui/dialogs/AboutDialog.cpp
+++ b/launcher/ui/dialogs/AboutDialog.cpp
@@ -71,13 +71,18 @@ QString getCreditsHtml()
//: %1 is the name of the launcher, determined at build time, e.g. "Prism Launcher Developers"
stream << "<h3>" << QObject::tr("%1 Developers", "About Credits").arg(BuildConfig.LAUNCHER_DISPLAYNAME) << "</h3>\n";
stream << QString("<p>Sefa Eyeoglu (Scrumplex) %1</p>\n") .arg(getWebsite("https://scrumplex.net"));
- stream << QString("<p>dada513 %1</p>\n") .arg(getGitHub("dada513"));
+ stream << QString("<p>d-513 %1</p>\n") .arg(getGitHub("d-513"));
stream << QString("<p>txtsd %1</p>\n") .arg(getWebsite("https://ihavea.quest"));
stream << QString("<p>timoreo %1</p>\n") .arg(getGitHub("timoreo22"));
stream << QString("<p>Ezekiel Smith (ZekeSmith) %1</p>\n") .arg(getGitHub("ZekeSmith"));
stream << QString("<p>cozyGalvinism %1</p>\n") .arg(getGitHub("cozyGalvinism"));
- stream << QString("<p>DioEgizio %1</p>\n") .arg(getGitHub("DioEgizio"));
- stream << QString("<p>flowln %1</p>\n") .arg(getGitHub("flowln"));
+ stream << QString("<p>DioEgizio %1</p>\n") .arg(getGitHub("DioEgizio"));
+ stream << QString("<p>flowln %1</p>\n") .arg(getGitHub("flowln"));
+ stream << QString("<p>ViRb3 %1</p>\n") .arg(getGitHub("ViRb3"));
+ stream << QString("<p>Rachel Powers (Ryex) %1</p>\n") .arg(getGitHub("Ryex"));
+ stream << QString("<p>TayouVR %1</p>\n") .arg(getGitHub("TayouVR"));
+ stream << QString("<p>TheKodeToad %1</p>\n") .arg(getGitHub("TheKodeToad"));
+ stream << QString("<p>getchoo %1</p>\n") .arg(getGitHub("getchoo"));
stream << "<br />\n";
// TODO: possibly retrieve from git history at build time?
diff --git a/launcher/ui/dialogs/BlockedModsDialog.cpp b/launcher/ui/dialogs/BlockedModsDialog.cpp
index ba453df6..fdfae597 100644
--- a/launcher/ui/dialogs/BlockedModsDialog.cpp
+++ b/launcher/ui/dialogs/BlockedModsDialog.cpp
@@ -195,7 +195,7 @@ void BlockedModsDialog::watchPath(QString path, bool watch_recursive)
auto to_watch = QFileInfo(path);
auto to_watch_path = to_watch.canonicalFilePath();
if (m_watcher.directories().contains(to_watch_path))
- return; // don't watch the same path twice (no loops!)
+ return; // don't watch the same path twice (no loops!)
qDebug() << "[Blocked Mods Dialog] Adding Watch Path:" << path;
m_watcher.addPath(to_watch_path);
@@ -203,10 +203,9 @@ void BlockedModsDialog::watchPath(QString path, bool watch_recursive)
if (!to_watch.isDir() || !watch_recursive)
return;
-
QDirIterator it(to_watch_path, QDir::Filter::Dirs | QDir::Filter::NoDotAndDotDot, QDirIterator::NoIteratorFlags);
while (it.hasNext()) {
- QString watch_dir = QDir(it.next()).canonicalPath(); // resolve symlinks and relative paths
+ QString watch_dir = QDir(it.next()).canonicalPath(); // resolve symlinks and relative paths
watchPath(watch_dir, watch_recursive);
}
}
@@ -302,11 +301,35 @@ bool BlockedModsDialog::checkValidPath(QString path)
{
const QFileInfo file = QFileInfo(path);
const QString filename = file.fileName();
- QString laxFilename(filename);
- laxFilename.replace('+', ' ');
- auto compare = [](QString fsfilename, QString metadataFilename) {
- return metadataFilename.compare(fsfilename, Qt::CaseInsensitive) == 0;
+ auto compare = [](QString fsFilename, QString metadataFilename) {
+ return metadataFilename.compare(fsFilename, Qt::CaseInsensitive) == 0;
+ };
+
+ // super lax compare (but not fuzzy)
+ // convert to lowercase
+ // convert all speratores to whitespace
+ // simplify sequence of internal whitespace to a single space
+ // efectivly compare two strings ignoring all separators and case
+ auto laxCompare = [](QString fsfilename, QString metadataFilename) {
+ // allowed character seperators
+ QList<QChar> allowedSeperators = { '-', '+', '.' , '_'};
+
+ // copy in lowercase
+ auto fsName = fsfilename.toLower();
+ auto metaName = metadataFilename.toLower();
+
+ // replace all potential allowed seperatores with whitespace
+ for (auto sep : allowedSeperators) {
+ fsName = fsName.replace(sep, ' ');
+ metaName = metaName.replace(sep, ' ');
+ }
+
+ // remove extraneous whitespace
+ fsName = fsName.simplified();
+ metaName = metaName.simplified();
+
+ return fsName.compare(metaName) == 0;
};
for (auto& mod : m_mods) {
@@ -314,7 +337,7 @@ bool BlockedModsDialog::checkValidPath(QString path)
qDebug() << "[Blocked Mods Dialog] Name match found:" << mod.name << "| From path:" << path;
return true;
}
- if (compare(laxFilename, mod.name)) {
+ if (laxCompare(filename, mod.name)) {
qDebug() << "[Blocked Mods Dialog] Lax name match found:" << mod.name << "| From path:" << path;
return true;
}
diff --git a/launcher/ui/dialogs/CopyInstanceDialog.cpp b/launcher/ui/dialogs/CopyInstanceDialog.cpp
index 3f5122f6..d75bb5fe 100644
--- a/launcher/ui/dialogs/CopyInstanceDialog.cpp
+++ b/launcher/ui/dialogs/CopyInstanceDialog.cpp
@@ -37,18 +37,21 @@
#include <QPushButton>
#include "Application.h"
+#include "BuildConfig.h"
#include "CopyInstanceDialog.h"
#include "ui_CopyInstanceDialog.h"
#include "ui/dialogs/IconPickerDialog.h"
-#include "BaseVersion.h"
-#include "icons/IconList.h"
#include "BaseInstance.h"
+#include "BaseVersion.h"
+#include "DesktopServices.h"
+#include "FileSystem.h"
#include "InstanceList.h"
+#include "icons/IconList.h"
-CopyInstanceDialog::CopyInstanceDialog(InstancePtr original, QWidget *parent)
- :QDialog(parent), ui(new Ui::CopyInstanceDialog), m_original(original)
+CopyInstanceDialog::CopyInstanceDialog(InstancePtr original, QWidget* parent)
+ : QDialog(parent), ui(new Ui::CopyInstanceDialog), m_original(original)
{
ui->setupUi(this);
resize(minimumSizeHint());
@@ -71,8 +74,7 @@ CopyInstanceDialog::CopyInstanceDialog(InstancePtr original, QWidget *parent)
groupList.push_front("");
ui->groupBox->addItems(groupList);
int index = groupList.indexOf(APPLICATION->instances()->getInstanceGroup(m_original->id()));
- if(index == -1)
- {
+ if (index == -1) {
index = 0;
}
ui->groupBox->setCurrentIndex(index);
@@ -85,6 +87,35 @@ CopyInstanceDialog::CopyInstanceDialog(InstancePtr original, QWidget *parent)
ui->copyServersCheckbox->setChecked(m_selectedOptions.isCopyServersEnabled());
ui->copyModsCheckbox->setChecked(m_selectedOptions.isCopyModsEnabled());
ui->copyScreenshotsCheckbox->setChecked(m_selectedOptions.isCopyScreenshotsEnabled());
+
+ ui->symbolicLinksCheckbox->setChecked(m_selectedOptions.isUseSymLinksEnabled());
+ ui->hardLinksCheckbox->setChecked(m_selectedOptions.isUseHardLinksEnabled());
+
+ ui->recursiveLinkCheckbox->setChecked(m_selectedOptions.isLinkRecursivelyEnabled());
+ ui->dontLinkSavesCheckbox->setChecked(m_selectedOptions.isDontLinkSavesEnabled());
+
+ auto detectedFS = FS::statFS(m_original->instanceRoot()).fsType;
+
+ m_cloneSupported = FS::canCloneOnFS(detectedFS);
+ m_linkSupported = FS::canLinkOnFS(detectedFS);
+
+ if (m_cloneSupported) {
+ ui->cloneSupportedLabel->setText(tr("Reflinks are supported on %1").arg(FS::getFilesystemTypeName(detectedFS)));
+ } else {
+ ui->cloneSupportedLabel->setText(tr("Reflinks aren't supported on %1").arg(FS::getFilesystemTypeName(detectedFS)));
+ }
+
+#if defined(Q_OS_WIN)
+ ui->symbolicLinksCheckbox->setIcon(style()->standardIcon(QStyle::SP_VistaShield));
+ ui->symbolicLinksCheckbox->setToolTip(tr("Use symbolic links instead of copying files.") +
+ "\n" + tr("On Windows, symbolic links may require admin permission to create."));
+#endif
+
+ updateLinkOptions();
+ updateUseCloneCheckbox();
+
+ auto HelpButton = ui->buttonBox->button(QDialogButtonBox::Help);
+ connect(HelpButton, &QPushButton::clicked, this, &CopyInstanceDialog::help);
}
CopyInstanceDialog::~CopyInstanceDialog()
@@ -96,8 +127,7 @@ void CopyInstanceDialog::updateDialogState()
{
auto allowOK = !instName().isEmpty();
auto OkButton = ui->buttonBox->button(QDialogButtonBox::Ok);
- if(OkButton->isEnabled() != allowOK)
- {
+ if (OkButton->isEnabled() != allowOK) {
OkButton->setEnabled(allowOK);
}
}
@@ -105,8 +135,7 @@ void CopyInstanceDialog::updateDialogState()
QString CopyInstanceDialog::instName() const
{
auto result = ui->instNameTextBox->text().trimmed();
- if(result.size())
- {
+ if (result.size()) {
return result;
}
return QString();
@@ -127,6 +156,11 @@ const InstanceCopyPrefs& CopyInstanceDialog::getChosenOptions() const
return m_selectedOptions;
}
+void CopyInstanceDialog::help()
+{
+ DesktopServices::openUrl(QUrl(BuildConfig.HELP_URL.arg("instance-copy")));
+}
+
void CopyInstanceDialog::checkAllCheckboxes(const bool& b)
{
ui->keepPlaytimeCheckbox->setChecked(b);
@@ -147,20 +181,46 @@ void CopyInstanceDialog::updateSelectAllCheckbox()
ui->selectAllCheckbox->blockSignals(false);
}
+void CopyInstanceDialog::updateUseCloneCheckbox()
+{
+ ui->useCloneCheckbox->setEnabled(m_cloneSupported && !ui->symbolicLinksCheckbox->isChecked() && !ui->hardLinksCheckbox->isChecked());
+ ui->useCloneCheckbox->setChecked(m_cloneSupported && m_selectedOptions.isUseCloneEnabled() && !ui->symbolicLinksCheckbox->isChecked() &&
+ !ui->hardLinksCheckbox->isChecked());
+}
+
+void CopyInstanceDialog::updateLinkOptions()
+{
+ ui->symbolicLinksCheckbox->setEnabled(m_linkSupported && !ui->hardLinksCheckbox->isChecked() && !ui->useCloneCheckbox->isChecked());
+ ui->hardLinksCheckbox->setEnabled(m_linkSupported && !ui->symbolicLinksCheckbox->isChecked() && !ui->useCloneCheckbox->isChecked());
+
+ ui->symbolicLinksCheckbox->setChecked(m_linkSupported && m_selectedOptions.isUseSymLinksEnabled() &&
+ !ui->useCloneCheckbox->isChecked());
+ ui->hardLinksCheckbox->setChecked(m_linkSupported && m_selectedOptions.isUseHardLinksEnabled() && !ui->useCloneCheckbox->isChecked());
+
+ bool linksInUse = (ui->symbolicLinksCheckbox->isChecked() || ui->hardLinksCheckbox->isChecked());
+ ui->recursiveLinkCheckbox->setEnabled(m_linkSupported && linksInUse && !ui->hardLinksCheckbox->isChecked());
+ ui->dontLinkSavesCheckbox->setEnabled(m_linkSupported && linksInUse);
+ ui->recursiveLinkCheckbox->setChecked(m_linkSupported && linksInUse && m_selectedOptions.isLinkRecursivelyEnabled());
+ ui->dontLinkSavesCheckbox->setChecked(m_linkSupported && linksInUse && m_selectedOptions.isDontLinkSavesEnabled());
+
+#if defined(Q_OS_WIN)
+ auto OkButton = ui->buttonBox->button(QDialogButtonBox::Ok);
+ OkButton->setIcon(m_selectedOptions.isUseSymLinksEnabled() ? style()->standardIcon(QStyle::SP_VistaShield) : QIcon());
+#endif
+}
+
void CopyInstanceDialog::on_iconButton_clicked()
{
IconPickerDialog dlg(this);
dlg.execWithSelection(InstIconKey);
- if (dlg.result() == QDialog::Accepted)
- {
+ if (dlg.result() == QDialog::Accepted) {
InstIconKey = dlg.selectedIconKey;
ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey));
}
}
-
-void CopyInstanceDialog::on_instNameTextBox_textChanged(const QString &arg1)
+void CopyInstanceDialog::on_instNameTextBox_textChanged(const QString& arg1)
{
updateDialogState();
}
@@ -175,10 +235,10 @@ void CopyInstanceDialog::on_selectAllCheckbox_stateChanged(int state)
void CopyInstanceDialog::on_copySavesCheckbox_stateChanged(int state)
{
m_selectedOptions.enableCopySaves(state == Qt::Checked);
+ ui->dontLinkSavesCheckbox->setChecked((state == Qt::Checked) && ui->dontLinkSavesCheckbox->isChecked());
updateSelectAllCheckbox();
}
-
void CopyInstanceDialog::on_keepPlaytimeCheckbox_stateChanged(int state)
{
m_selectedOptions.enableKeepPlaytime(state == Qt::Checked);
@@ -220,3 +280,38 @@ void CopyInstanceDialog::on_copyScreenshotsCheckbox_stateChanged(int state)
m_selectedOptions.enableCopyScreenshots(state == Qt::Checked);
updateSelectAllCheckbox();
}
+
+void CopyInstanceDialog::on_symbolicLinksCheckbox_stateChanged(int state)
+{
+ m_selectedOptions.enableUseSymLinks(state == Qt::Checked);
+ updateUseCloneCheckbox();
+ updateLinkOptions();
+}
+
+void CopyInstanceDialog::on_hardLinksCheckbox_stateChanged(int state)
+{
+ m_selectedOptions.enableUseHardLinks(state == Qt::Checked);
+ if (state == Qt::Checked && !ui->recursiveLinkCheckbox->isChecked()) {
+ ui->recursiveLinkCheckbox->setChecked(true);
+ }
+ updateUseCloneCheckbox();
+ updateLinkOptions();
+}
+
+void CopyInstanceDialog::on_recursiveLinkCheckbox_stateChanged(int state)
+{
+ m_selectedOptions.enableLinkRecursively(state == Qt::Checked);
+ updateLinkOptions();
+}
+
+void CopyInstanceDialog::on_dontLinkSavesCheckbox_stateChanged(int state)
+{
+ m_selectedOptions.enableDontLinkSaves(state == Qt::Checked);
+}
+
+void CopyInstanceDialog::on_useCloneCheckbox_stateChanged(int state)
+{
+ m_selectedOptions.enableUseClone(m_cloneSupported && (state == Qt::Checked));
+ updateUseCloneCheckbox();
+ updateLinkOptions();
+}
diff --git a/launcher/ui/dialogs/CopyInstanceDialog.h b/launcher/ui/dialogs/CopyInstanceDialog.h
index 884501d1..698c6e93 100644
--- a/launcher/ui/dialogs/CopyInstanceDialog.h
+++ b/launcher/ui/dialogs/CopyInstanceDialog.h
@@ -16,22 +16,21 @@
#pragma once
#include <QDialog>
+#include "BaseInstance.h"
#include "BaseVersion.h"
#include "InstanceCopyPrefs.h"
class BaseInstance;
-namespace Ui
-{
+namespace Ui {
class CopyInstanceDialog;
}
-class CopyInstanceDialog : public QDialog
-{
+class CopyInstanceDialog : public QDialog {
Q_OBJECT
-public:
- explicit CopyInstanceDialog(InstancePtr original, QWidget *parent = 0);
+ public:
+ explicit CopyInstanceDialog(InstancePtr original, QWidget* parent = 0);
~CopyInstanceDialog();
void updateDialogState();
@@ -41,10 +40,12 @@ public:
QString iconKey() const;
const InstanceCopyPrefs& getChosenOptions() const;
-private
-slots:
+ public slots:
+ void help();
+
+ private slots:
void on_iconButton_clicked();
- void on_instNameTextBox_textChanged(const QString &arg1);
+ void on_instNameTextBox_textChanged(const QString& arg1);
// Checkboxes
void on_selectAllCheckbox_stateChanged(int state);
void on_copySavesCheckbox_stateChanged(int state);
@@ -55,13 +56,23 @@ slots:
void on_copyServersCheckbox_stateChanged(int state);
void on_copyModsCheckbox_stateChanged(int state);
void on_copyScreenshotsCheckbox_stateChanged(int state);
+ void on_symbolicLinksCheckbox_stateChanged(int state);
+ void on_hardLinksCheckbox_stateChanged(int state);
+ void on_recursiveLinkCheckbox_stateChanged(int state);
+ void on_dontLinkSavesCheckbox_stateChanged(int state);
+ void on_useCloneCheckbox_stateChanged(int state);
-private:
+ private:
void checkAllCheckboxes(const bool& b);
void updateSelectAllCheckbox();
+ void updateUseCloneCheckbox();
+ void updateLinkOptions();
+
/* data */
- Ui::CopyInstanceDialog *ui;
+ Ui::CopyInstanceDialog* ui;
QString InstIconKey;
InstancePtr m_original;
InstanceCopyPrefs m_selectedOptions;
+ bool m_cloneSupported = false;
+ bool m_linkSupported = false;
};
diff --git a/launcher/ui/dialogs/CopyInstanceDialog.ui b/launcher/ui/dialogs/CopyInstanceDialog.ui
index b7828fe3..5060debc 100644
--- a/launcher/ui/dialogs/CopyInstanceDialog.ui
+++ b/launcher/ui/dialogs/CopyInstanceDialog.ui
@@ -9,8 +9,8 @@
<rect>
<x>0</x>
<y>0</y>
- <width>341</width>
- <height>399</height>
+ <width>575</width>
+ <height>695</height>
</rect>
</property>
<property name="windowTitle">
@@ -113,93 +113,268 @@
</layout>
</item>
<item>
- <layout class="QHBoxLayout" name="selectAllButtonLayout">
- <item>
- <widget class="QCheckBox" name="selectAllCheckbox">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="layoutDirection">
- <enum>Qt::LeftToRight</enum>
- </property>
- <property name="text">
- <string>Select all</string>
- </property>
- <property name="checked">
- <bool>false</bool>
- </property>
- </widget>
- </item>
- </layout>
+ <widget class="QGroupBox" name="copyOptionsGroup">
+ <property name="title">
+ <string>Instance Copy Options</string>
+ </property>
+ <layout class="QGridLayout" name="copyOptionsLayout">
+ <item row="1" column="0">
+ <widget class="QCheckBox" name="keepPlaytimeCheckbox">
+ <property name="text">
+ <string>Keep play time</string>
+ </property>
+ </widget>
+ </item>
+ <item row="6" column="1">
+ <widget class="QCheckBox" name="copyModsCheckbox">
+ <property name="toolTip">
+ <string>Disabling this will still keep the mod loader (ex: Fabric, Quilt, etc.) but erase the mods folder and their configs.</string>
+ </property>
+ <property name="text">
+ <string>Copy mods</string>
+ </property>
+ </widget>
+ </item>
+ <item row="6" column="0">
+ <widget class="QCheckBox" name="copyResPacksCheckbox">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="text">
+ <string>Copy resource packs</string>
+ </property>
+ </widget>
+ </item>
+ <item row="5" column="0">
+ <widget class="QCheckBox" name="copyGameOptionsCheckbox">
+ <property name="toolTip">
+ <string>Copy the in-game options like FOV, max framerate, etc.</string>
+ </property>
+ <property name="text">
+ <string>Copy game options</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="1">
+ <widget class="QCheckBox" name="copyShaderPacksCheckbox">
+ <property name="text">
+ <string>Copy shader packs</string>
+ </property>
+ </widget>
+ </item>
+ <item row="5" column="1">
+ <widget class="QCheckBox" name="copyServersCheckbox">
+ <property name="text">
+ <string>Copy servers</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="0">
+ <widget class="QCheckBox" name="copySavesCheckbox">
+ <property name="text">
+ <string>Copy saves</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QCheckBox" name="copyScreenshotsCheckbox">
+ <property name="text">
+ <string>Copy screenshots</string>
+ </property>
+ </widget>
+ </item>
+ <item row="7" column="1">
+ <widget class="QCheckBox" name="selectAllCheckbox">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="layoutDirection">
+ <enum>Qt::LeftToRight</enum>
+ </property>
+ <property name="text">
+ <string>Select all</string>
+ </property>
+ <property name="checked">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
</item>
<item>
- <layout class="QGridLayout" name="copyOptionsLayout">
- <item row="6" column="1">
- <widget class="QCheckBox" name="copyModsCheckbox">
- <property name="toolTip">
- <string>Disabling this will still keep the mod loader (ex: Fabric, Quilt, etc.) but erase the mods folder and their configs.</string>
- </property>
- <property name="text">
- <string>Copy mods</string>
- </property>
- </widget>
- </item>
- <item row="5" column="0">
- <widget class="QCheckBox" name="copyGameOptionsCheckbox">
+ <widget class="Line" name="line_2">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="advancedOptionsLabel">
+ <property name="text">
+ <string>Advanced Copy Options</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <layout class="QVBoxLayout" name="copyModeLayout">
+ <item>
+ <widget class="QGroupBox" name="linkFilesGroup">
<property name="toolTip">
- <string>Copy the in-game options like FOV, max framerate, etc.</string>
+ <string>Use symbolic or hard links instead of copying files.</string>
</property>
- <property name="text">
- <string>Copy game options</string>
- </property>
- </widget>
- </item>
- <item row="3" column="0">
- <widget class="QCheckBox" name="copySavesCheckbox">
- <property name="text">
- <string>Copy saves</string>
- </property>
- </widget>
- </item>
- <item row="3" column="1">
- <widget class="QCheckBox" name="copyShaderPacksCheckbox">
- <property name="text">
- <string>Copy shader packs</string>
- </property>
- </widget>
- </item>
- <item row="5" column="1">
- <widget class="QCheckBox" name="copyServersCheckbox">
- <property name="text">
- <string>Copy servers</string>
+ <property name="title">
+ <string>Symbolic and Hard Link Options</string>
</property>
- </widget>
- </item>
- <item row="6" column="0">
- <widget class="QCheckBox" name="copyResPacksCheckbox">
- <property name="enabled">
- <bool>true</bool>
+ <property name="flat">
+ <bool>false</bool>
</property>
- <property name="text">
- <string>Copy resource packs</string>
+ <property name="checkable">
+ <bool>false</bool>
</property>
- </widget>
- </item>
- <item row="1" column="0">
- <widget class="QCheckBox" name="keepPlaytimeCheckbox">
- <property name="text">
- <string>Keep play time</string>
+ <property name="checked">
+ <bool>false</bool>
</property>
+ <layout class="QVBoxLayout" name="linkOptionsLayout">
+ <item>
+ <widget class="QLabel" name="linkOptionsLabel">
+ <property name="text">
+ <string>Links are supported on most filesystems except FAT</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <layout class="QGridLayout" name="linkOptionsGridLayout" rowstretch="0,0,0,0" columnstretch="0,0" rowminimumheight="0,0,0,0" columnminimumwidth="0,0">
+ <property name="leftMargin">
+ <number>6</number>
+ </property>
+ <property name="topMargin">
+ <number>6</number>
+ </property>
+ <property name="rightMargin">
+ <number>6</number>
+ </property>
+ <property name="bottomMargin">
+ <number>6</number>
+ </property>
+ <item row="2" column="1">
+ <widget class="QCheckBox" name="recursiveLinkCheckbox">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="toolTip">
+ <string>Link each resource individually instead of linking whole folders at once</string>
+ </property>
+ <property name="text">
+ <string>Link files recursively</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="1">
+ <widget class="QCheckBox" name="dontLinkSavesCheckbox">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="toolTip">
+ <string>If &quot;copy saves&quot; is selected world save data will be copied instead of linked and thus not shared between instances.</string>
+ </property>
+ <property name="text">
+ <string>Don't link saves</string>
+ </property>
+ <property name="checked">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="0">
+ <widget class="QCheckBox" name="hardLinksCheckbox">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="toolTip">
+ <string>Use hard links instead of copying files.</string>
+ </property>
+ <property name="text">
+ <string>Use hard links</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QCheckBox" name="symbolicLinksCheckbox">
+ <property name="toolTip">
+ <string>Use symbolic links instead of copying files.</string>
+ </property>
+ <property name="text">
+ <string>Use symbolic links</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
</widget>
</item>
- <item row="1" column="1">
- <widget class="QCheckBox" name="copyScreenshotsCheckbox">
- <property name="text">
- <string>Copy screenshots</string>
+ <item>
+ <widget class="QGroupBox" name="horizontalGroupBox">
+ <property name="title">
+ <string>CoW (Copy-on-Write) Options</string>
</property>
+ <layout class="QHBoxLayout" name="useCloneLayout">
+ <item>
+ <widget class="QCheckBox" name="useCloneCheckbox">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="toolTip">
+ <string>Files cloned with reflinks take up no extra space until they are modified.</string>
+ </property>
+ <property name="text">
+ <string>Clone instead of copying</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="CoWSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="cloneSupportedLabel">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+ <horstretch>1</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Your filesystem and/or OS doesn't support reflinks</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ <property name="margin">
+ <number>4</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
</widget>
</item>
</layout>
@@ -210,7 +385,7 @@
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
- <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Help|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
@@ -220,10 +395,21 @@
<tabstop>iconButton</tabstop>
<tabstop>instNameTextBox</tabstop>
<tabstop>groupBox</tabstop>
+ <tabstop>keepPlaytimeCheckbox</tabstop>
+ <tabstop>copyScreenshotsCheckbox</tabstop>
+ <tabstop>copySavesCheckbox</tabstop>
+ <tabstop>copyShaderPacksCheckbox</tabstop>
+ <tabstop>copyGameOptionsCheckbox</tabstop>
+ <tabstop>copyServersCheckbox</tabstop>
+ <tabstop>copyResPacksCheckbox</tabstop>
+ <tabstop>copyModsCheckbox</tabstop>
+ <tabstop>symbolicLinksCheckbox</tabstop>
+ <tabstop>recursiveLinkCheckbox</tabstop>
+ <tabstop>hardLinksCheckbox</tabstop>
+ <tabstop>dontLinkSavesCheckbox</tabstop>
+ <tabstop>useCloneCheckbox</tabstop>
</tabstops>
- <resources>
- <include location="../../graphics.qrc"/>
- </resources>
+ <resources/>
<connections>
<connection>
<sender>buttonBox</sender>
@@ -232,8 +418,8 @@
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
- <x>254</x>
- <y>316</y>
+ <x>269</x>
+ <y>692</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
@@ -248,8 +434,8 @@
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
- <x>322</x>
- <y>316</y>
+ <x>337</x>
+ <y>692</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
diff --git a/launcher/ui/dialogs/ExportInstanceDialog.cpp b/launcher/ui/dialogs/ExportInstanceDialog.cpp
index f13e36e8..cc41c394 100644
--- a/launcher/ui/dialogs/ExportInstanceDialog.cpp
+++ b/launcher/ui/dialogs/ExportInstanceDialog.cpp
@@ -1,7 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
- * PolyMC - Minecraft Launcher
+ * Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ * Copyright (C) 2023 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
@@ -34,311 +35,39 @@
*/
#include "ExportInstanceDialog.h"
-#include "ui_ExportInstanceDialog.h"
#include <BaseInstance.h>
#include <MMCZip.h>
#include <QFileDialog>
-#include <QMessageBox>
#include <QFileSystemModel>
+#include <QMessageBox>
+#include "FileIgnoreProxy.h"
+#include "ui_ExportInstanceDialog.h"
-#include <QSortFilterProxyModel>
+#include <FileSystem.h>
+#include <icons/IconList.h>
#include <QDebug>
+#include <QFileInfo>
#include <QSaveFile>
+#include <QSortFilterProxyModel>
#include <QStack>
-#include "StringUtils.h"
-#include "SeparatorPrefixTree.h"
+#include <functional>
#include "Application.h"
-#include <icons/IconList.h>
-#include <FileSystem.h>
-
-class PackIgnoreProxy : public QSortFilterProxyModel
-{
- Q_OBJECT
-
-public:
- PackIgnoreProxy(InstancePtr instance, QObject *parent) : QSortFilterProxyModel(parent)
- {
- m_instance = instance;
- }
- // NOTE: Sadly, we have to do sorting ourselves.
- bool lessThan(const QModelIndex &left, const QModelIndex &right) const
- {
- QFileSystemModel *fsm = qobject_cast<QFileSystemModel *>(sourceModel());
- if (!fsm)
- {
- return QSortFilterProxyModel::lessThan(left, right);
- }
- bool asc = sortOrder() == Qt::AscendingOrder ? true : false;
-
- QFileInfo leftFileInfo = fsm->fileInfo(left);
- QFileInfo rightFileInfo = fsm->fileInfo(right);
-
- if (!leftFileInfo.isDir() && rightFileInfo.isDir())
- {
- return !asc;
- }
- if (leftFileInfo.isDir() && !rightFileInfo.isDir())
- {
- return asc;
- }
-
- // sort and proxy model breaks the original model...
- if (sortColumn() == 0)
- {
- return StringUtils::naturalCompare(leftFileInfo.fileName(), rightFileInfo.fileName(),
- Qt::CaseInsensitive) < 0;
- }
- if (sortColumn() == 1)
- {
- auto leftSize = leftFileInfo.size();
- auto rightSize = rightFileInfo.size();
- if ((leftSize == rightSize) || (leftFileInfo.isDir() && rightFileInfo.isDir()))
- {
- return StringUtils::naturalCompare(leftFileInfo.fileName(),
- rightFileInfo.fileName(),
- Qt::CaseInsensitive) < 0
- ? asc
- : !asc;
- }
- return leftSize < rightSize;
- }
- return QSortFilterProxyModel::lessThan(left, right);
- }
-
- virtual Qt::ItemFlags flags(const QModelIndex &index) const
- {
- if (!index.isValid())
- return Qt::NoItemFlags;
-
- auto sourceIndex = mapToSource(index);
- Qt::ItemFlags flags = sourceIndex.flags();
- if (index.column() == 0)
- {
- flags |= Qt::ItemIsUserCheckable;
- if (sourceIndex.model()->hasChildren(sourceIndex))
- {
- flags |= Qt::ItemIsAutoTristate;
- }
- }
-
- return flags;
- }
-
- virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const
- {
- QModelIndex sourceIndex = mapToSource(index);
-
- if (index.column() == 0 && role == Qt::CheckStateRole)
- {
- QFileSystemModel *fsm = qobject_cast<QFileSystemModel *>(sourceModel());
- auto blockedPath = relPath(fsm->filePath(sourceIndex));
- auto cover = blocked.cover(blockedPath);
- if (!cover.isNull())
- {
- return QVariant(Qt::Unchecked);
- }
- else if (blocked.exists(blockedPath))
- {
- return QVariant(Qt::PartiallyChecked);
- }
- else
- {
- return QVariant(Qt::Checked);
- }
- }
-
- return sourceIndex.data(role);
- }
-
- virtual bool setData(const QModelIndex &index, const QVariant &value,
- int role = Qt::EditRole)
- {
- if (index.column() == 0 && role == Qt::CheckStateRole)
- {
- Qt::CheckState state = static_cast<Qt::CheckState>(value.toInt());
- return setFilterState(index, state);
- }
-
- QModelIndex sourceIndex = mapToSource(index);
- return QSortFilterProxyModel::sourceModel()->setData(sourceIndex, value, role);
- }
-
- QString relPath(const QString &path) const
- {
- QString prefix = QDir().absoluteFilePath(m_instance->instanceRoot());
- prefix += '/';
- if (!path.startsWith(prefix))
- {
- return QString();
- }
- return path.mid(prefix.size());
- }
-
- bool setFilterState(QModelIndex index, Qt::CheckState state)
- {
- QFileSystemModel *fsm = qobject_cast<QFileSystemModel *>(sourceModel());
-
- if (!fsm)
- {
- return false;
- }
-
- QModelIndex sourceIndex = mapToSource(index);
- auto blockedPath = relPath(fsm->filePath(sourceIndex));
- bool changed = false;
- if (state == Qt::Unchecked)
- {
- // blocking a path
- auto &node = blocked.insert(blockedPath);
- // get rid of all blocked nodes below
- node.clear();
- changed = true;
- }
- else if (state == Qt::Checked || state == Qt::PartiallyChecked)
- {
- if (!blocked.remove(blockedPath))
- {
- auto cover = blocked.cover(blockedPath);
- qDebug() << "Blocked by cover" << cover;
- // uncover
- blocked.remove(cover);
- // block all contents, except for any cover
- QModelIndex rootIndex =
- fsm->index(FS::PathCombine(m_instance->instanceRoot(), cover));
- QModelIndex doing = rootIndex;
- int row = 0;
- QStack<QModelIndex> todo;
- while (1)
- {
- auto node = fsm->index(row, 0, doing);
- if (!node.isValid())
- {
- if (!todo.size())
- {
- break;
- }
- else
- {
- doing = todo.pop();
- row = 0;
- continue;
- }
- }
- auto relpath = relPath(fsm->filePath(node));
- if (blockedPath.startsWith(relpath)) // cover found?
- {
- // continue processing cover later
- todo.push(node);
- }
- else
- {
- // or just block this one.
- blocked.insert(relpath);
- }
- row++;
- }
- }
- changed = true;
- }
- if (changed)
- {
- // update the thing
- emit dataChanged(index, index, {Qt::CheckStateRole});
- // update everything above index
- QModelIndex up = index.parent();
- while (1)
- {
- if (!up.isValid())
- break;
- emit dataChanged(up, up, {Qt::CheckStateRole});
- up = up.parent();
- }
- // and everything below the index
- QModelIndex doing = index;
- int row = 0;
- QStack<QModelIndex> todo;
- while (1)
- {
- auto node = this->index(row, 0, doing);
- if (!node.isValid())
- {
- if (!todo.size())
- {
- break;
- }
- else
- {
- doing = todo.pop();
- row = 0;
- continue;
- }
- }
- emit dataChanged(node, node, {Qt::CheckStateRole});
- todo.push(node);
- row++;
- }
- // siblings and unrelated nodes are ignored
- }
- return true;
- }
-
- bool shouldExpand(QModelIndex index)
- {
- QModelIndex sourceIndex = mapToSource(index);
- QFileSystemModel *fsm = qobject_cast<QFileSystemModel *>(sourceModel());
- if (!fsm)
- {
- return false;
- }
- auto blockedPath = relPath(fsm->filePath(sourceIndex));
- auto found = blocked.find(blockedPath);
- if(found)
- {
- return !found->leaf();
- }
- return false;
- }
-
- void setBlockedPaths(QStringList paths)
- {
- beginResetModel();
- blocked.clear();
- blocked.insert(paths);
- endResetModel();
- }
-
- const SeparatorPrefixTree<'/'> & blockedPaths() const
- {
- return blocked;
- }
-
-protected:
- bool filterAcceptsColumn(int source_column, const QModelIndex &source_parent) const
- {
- Q_UNUSED(source_parent)
-
- // adjust the columns you want to filter out here
- // return false for those that will be hidden
- if (source_column == 2 || source_column == 3)
- return false;
-
- return true;
- }
-
-private:
- InstancePtr m_instance;
- SeparatorPrefixTree<'/'> blocked;
-};
+#include "SeparatorPrefixTree.h"
-ExportInstanceDialog::ExportInstanceDialog(InstancePtr instance, QWidget *parent)
+ExportInstanceDialog::ExportInstanceDialog(InstancePtr instance, QWidget* parent)
: QDialog(parent), ui(new Ui::ExportInstanceDialog), m_instance(instance)
{
ui->setupUi(this);
auto model = new QFileSystemModel(this);
- proxyModel = new PackIgnoreProxy(m_instance, this);
- loadPackIgnore();
- proxyModel->setSourceModel(model);
+ model->setIconProvider(&icons);
auto root = instance->instanceRoot();
+ proxyModel = new FileIgnoreProxy(root, this);
+ proxyModel->setSourceModel(model);
+ auto prefix = QDir(instance->instanceRoot()).relativeFilePath(instance->gameRoot());
+ proxyModel->ignoreFilesWithPath().insert({ FS::PathCombine(prefix, "logs"), FS::PathCombine(prefix, "crash-reports") });
+ proxyModel->ignoreFilesWithName().append({ ".DS_Store", "thumbs.db", "Thumbs.db" });
+ loadPackIgnore();
+
ui->treeView->setModel(proxyModel);
ui->treeView->setRootIndex(proxyModel->mapFromSource(model->index(root)));
ui->treeView->sortByColumn(0, Qt::AscendingOrder);
@@ -402,34 +131,22 @@ bool ExportInstanceDialog::doExport()
const QString output = QFileDialog::getSaveFileName(
this, tr("Export %1").arg(m_instance->name()),
- FS::PathCombine(QDir::homePath(), name + ".zip"), "Zip (*.zip)", nullptr, QFileDialog::DontConfirmOverwrite);
+ FS::PathCombine(QDir::homePath(), name + ".zip"), "Zip (*.zip)", nullptr);
if (output.isEmpty())
{
return false;
}
- if (QFile::exists(output))
- {
- int ret =
- QMessageBox::question(this, tr("Overwrite?"),
- tr("This file already exists. Do you want to overwrite it?"),
- QMessageBox::No, QMessageBox::Yes);
- if (ret == QMessageBox::No)
- {
- return false;
- }
- }
SaveIcon(m_instance);
- auto & blocked = proxyModel->blockedPaths();
- using std::placeholders::_1;
auto files = QFileInfoList();
if (!MMCZip::collectFileListRecursively(m_instance->instanceRoot(), nullptr, &files,
- std::bind(&SeparatorPrefixTree<'/'>::covers, blocked, _1))) {
+ std::bind(&FileIgnoreProxy::filterFile, proxyModel, std::placeholders::_1))) {
QMessageBox::warning(this, tr("Error"), tr("Unable to export instance"));
return false;
}
- if (!MMCZip::compressDirFiles(output, m_instance->instanceRoot(), files))
+
+ if (!MMCZip::compressDirFiles(output, m_instance->instanceRoot(), files, true))
{
QMessageBox::warning(this, tr("Error"), tr("Unable to export instance"));
return false;
@@ -508,5 +225,3 @@ void ExportInstanceDialog::savePackIgnore()
qWarning() << e.cause();
}
}
-
-#include "ExportInstanceDialog.moc"
diff --git a/launcher/ui/dialogs/ExportInstanceDialog.h b/launcher/ui/dialogs/ExportInstanceDialog.h
index dea02d1b..5e801875 100644
--- a/launcher/ui/dialogs/ExportInstanceDialog.h
+++ b/launcher/ui/dialogs/ExportInstanceDialog.h
@@ -1,16 +1,36 @@
-/* Copyright 2013-2021 MultiMC Contributors
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
*
- * 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
+ * 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.
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
*
- * Unless required by applicable law or agreed to in writing, software
- * 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.
+ * 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
@@ -18,9 +38,10 @@
#include <QDialog>
#include <QModelIndex>
#include <memory>
+#include "FileIgnoreProxy.h"
+#include "FastFileIconProvider.h"
class BaseInstance;
-class PackIgnoreProxy;
typedef std::shared_ptr<BaseInstance> InstancePtr;
namespace Ui
@@ -47,7 +68,8 @@ private:
private:
Ui::ExportInstanceDialog *ui;
InstancePtr m_instance;
- PackIgnoreProxy * proxyModel;
+ FileIgnoreProxy * proxyModel;
+ FastFileIconProvider icons;
private slots:
void rowsInserted(QModelIndex parent, int top, int bottom);
diff --git a/launcher/ui/dialogs/ExportPackDialog.cpp b/launcher/ui/dialogs/ExportPackDialog.cpp
new file mode 100644
index 00000000..2abe2805
--- /dev/null
+++ b/launcher/ui/dialogs/ExportPackDialog.cpp
@@ -0,0 +1,146 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2023 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 "ExportPackDialog.h"
+#include "minecraft/mod/ModFolderModel.h"
+#include "modplatform/ModIndex.h"
+#include "modplatform/flame/FlamePackExportTask.h"
+#include "ui/dialogs/CustomMessageBox.h"
+#include "ui/dialogs/ProgressDialog.h"
+#include "ui_ExportPackDialog.h"
+
+#include <QFileDialog>
+#include <QFileSystemModel>
+#include <QJsonDocument>
+#include <QMessageBox>
+#include <QPushButton>
+#include "FastFileIconProvider.h"
+#include "FileSystem.h"
+#include "MMCZip.h"
+#include "modplatform/modrinth/ModrinthPackExportTask.h"
+
+ExportPackDialog::ExportPackDialog(InstancePtr instance, QWidget* parent, ModPlatform::ResourceProvider provider)
+ : QDialog(parent), instance(instance), ui(new Ui::ExportPackDialog), m_provider(provider)
+{
+ ui->setupUi(this);
+ ui->name->setText(instance->name());
+ if (m_provider == ModPlatform::ResourceProvider::MODRINTH) {
+ ui->summary->setText(instance->notes().split(QRegularExpression("\\r?\\n"))[0]);
+ setWindowTitle("Export Modrinth Pack");
+ } else {
+ setWindowTitle("Export CurseForge Pack");
+ ui->version->setText("");
+ ui->summaryLabel->setText("Author");
+ }
+
+ // ensure a valid pack is generated
+ // the name and version fields mustn't be empty
+ connect(ui->name, &QLineEdit::textEdited, this, &ExportPackDialog::validate);
+ connect(ui->version, &QLineEdit::textEdited, this, &ExportPackDialog::validate);
+ // the instance name can technically be empty
+ validate();
+
+ QFileSystemModel* model = new QFileSystemModel(this);
+ model->setIconProvider(&icons);
+
+ // use the game root - everything outside cannot be exported
+ const QDir root(instance->gameRoot());
+ proxy = new FileIgnoreProxy(instance->gameRoot(), this);
+ proxy->ignoreFilesWithPath().insert({ "logs", "crash-reports" });
+ proxy->ignoreFilesWithName().append({ ".DS_Store", "thumbs.db", "Thumbs.db" });
+ proxy->setSourceModel(model);
+
+ const QDir::Filters filter(QDir::AllEntries | QDir::NoDotAndDotDot | QDir::AllDirs | QDir::Hidden);
+
+ for (const QString& file : root.entryList(filter)) {
+ if (!(file == "mods" || file == "coremods" || file == "datapacks" || file == "config" || file == "options.txt" ||
+ file == "servers.dat"))
+ proxy->blockedPaths().insert(file);
+ }
+
+ MinecraftInstance* mcInstance = dynamic_cast<MinecraftInstance*>(instance.get());
+ if (mcInstance) {
+ mcInstance->loaderModList()->update();
+ const QDir index = mcInstance->loaderModList()->indexDir();
+ if (index.exists())
+ proxy->blockedPaths().insert(root.relativeFilePath(index.absolutePath()));
+ }
+
+ ui->treeView->setModel(proxy);
+ ui->treeView->setRootIndex(proxy->mapFromSource(model->index(instance->gameRoot())));
+ ui->treeView->sortByColumn(0, Qt::AscendingOrder);
+
+ model->setFilter(filter);
+ model->setRootPath(instance->gameRoot());
+
+ QHeaderView* headerView = ui->treeView->header();
+ headerView->setSectionResizeMode(QHeaderView::ResizeToContents);
+ headerView->setSectionResizeMode(0, QHeaderView::Stretch);
+}
+
+ExportPackDialog::~ExportPackDialog()
+{
+ delete ui;
+}
+
+void ExportPackDialog::done(int result)
+{
+ if (result == Accepted) {
+ const QString filename = FS::RemoveInvalidFilenameChars(ui->name->text());
+ QString output;
+ if (m_provider == ModPlatform::ResourceProvider::MODRINTH)
+ output = QFileDialog::getSaveFileName(this, tr("Export %1").arg(ui->name->text()),
+ FS::PathCombine(QDir::homePath(), filename + ".mrpack"), "Modrinth pack (*.mrpack *.zip)",
+ nullptr);
+ else
+ output = QFileDialog::getSaveFileName(this, tr("Export %1").arg(ui->name->text()),
+ FS::PathCombine(QDir::homePath(), filename + ".zip"), "CurseForge pack (*.zip)", nullptr);
+
+ if (output.isEmpty())
+ return;
+ Task* task;
+ if (m_provider == ModPlatform::ResourceProvider::MODRINTH)
+ task = new ModrinthPackExportTask(ui->name->text(), ui->version->text(), ui->summary->text(), instance, output,
+ std::bind(&FileIgnoreProxy::filterFile, proxy, std::placeholders::_1));
+ else
+ task = new FlamePackExportTask(ui->name->text(), ui->version->text(), ui->summary->text(), instance, output,
+ std::bind(&FileIgnoreProxy::filterFile, proxy, std::placeholders::_1));
+
+ connect(task, &Task::failed,
+ [this](const QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); });
+ connect(task, &Task::aborted, [this] {
+ CustomMessageBox::selectable(this, tr("Task aborted"), tr("The task has been aborted by the user."), QMessageBox::Information)
+ ->show();
+ });
+ connect(task, &Task::finished, [task] { task->deleteLater(); });
+
+ ProgressDialog progress(this);
+ progress.setSkipButton(true, tr("Abort"));
+ if (progress.execWithTask(task) != QDialog::Accepted)
+ return;
+ }
+
+ QDialog::done(result);
+}
+
+void ExportPackDialog::validate()
+{
+ const bool invalid =
+ ui->name->text().isEmpty() || ((m_provider == ModPlatform::ResourceProvider::MODRINTH) && ui->version->text().isEmpty());
+ ui->buttonBox->button(QDialogButtonBox::Ok)->setDisabled(invalid);
+}
diff --git a/launcher/ui/dialogs/ExportPackDialog.h b/launcher/ui/dialogs/ExportPackDialog.h
new file mode 100644
index 00000000..830c24d2
--- /dev/null
+++ b/launcher/ui/dialogs/ExportPackDialog.h
@@ -0,0 +1,49 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2023 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 "BaseInstance.h"
+#include "FastFileIconProvider.h"
+#include "FileIgnoreProxy.h"
+#include "modplatform/ModIndex.h"
+
+namespace Ui {
+class ExportPackDialog;
+}
+
+class ExportPackDialog : public QDialog {
+ Q_OBJECT
+
+ public:
+ explicit ExportPackDialog(InstancePtr instance,
+ QWidget* parent = nullptr,
+ ModPlatform::ResourceProvider provider = ModPlatform::ResourceProvider::MODRINTH);
+ ~ExportPackDialog();
+
+ void done(int result) override;
+ void validate();
+
+ private:
+ const InstancePtr instance;
+ Ui::ExportPackDialog* ui;
+ FileIgnoreProxy* proxy;
+ FastFileIconProvider icons;
+ const ModPlatform::ResourceProvider m_provider;
+};
diff --git a/launcher/ui/dialogs/ExportPackDialog.ui b/launcher/ui/dialogs/ExportPackDialog.ui
new file mode 100644
index 00000000..3976e28f
--- /dev/null
+++ b/launcher/ui/dialogs/ExportPackDialog.ui
@@ -0,0 +1,137 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ExportPackDialog</class>
+ <widget class="QDialog" name="ExportPackDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>650</width>
+ <height>413</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Export Pack</string>
+ </property>
+ <property name="sizeGripEnabled">
+ <bool>true</bool>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <widget class="QGroupBox" name="information">
+ <property name="title">
+ <string>Information</string>
+ </property>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="3" column="0">
+ <widget class="QLabel" name="summaryLabel">
+ <property name="text">
+ <string>Summary</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="1">
+ <widget class="QLineEdit" name="summary"/>
+ </item>
+ <item row="0" column="0">
+ <widget class="QLabel" name="nameLabel">
+ <property name="text">
+ <string>Name</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="versionLabel">
+ <property name="text">
+ <string>Version</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLineEdit" name="name"/>
+ </item>
+ <item row="1" column="1">
+ <widget class="QLineEdit" name="version">
+ <property name="text">
+ <string>1.0.0</string>
+ </property>
+ </widget>
+ </item>
+
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="filesLabel">
+ <property name="text">
+ <string>Files</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QTreeView" name="treeView">
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ <property name="selectionMode">
+ <enum>QAbstractItemView::ExtendedSelection</enum>
+ </property>
+ <property name="sortingEnabled">
+ <bool>true</bool>
+ </property>
+ <attribute name="headerStretchLastSection">
+ <bool>false</bool>
+ </attribute>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>name</tabstop>
+ <tabstop>version</tabstop>
+ <tabstop>summary</tabstop>
+ <tabstop>treeView</tabstop>
+ </tabstops>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>accepted()</signal>
+ <receiver>ExportPackDialog</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>324</x>
+ <y>390</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>324</x>
+ <y>206</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>rejected()</signal>
+ <receiver>ExportPackDialog</receiver>
+ <slot>reject()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>324</x>
+ <y>390</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>324</x>
+ <y>206</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>
diff --git a/launcher/ui/dialogs/ExportToModListDialog.cpp b/launcher/ui/dialogs/ExportToModListDialog.cpp
new file mode 100644
index 00000000..c811bfe6
--- /dev/null
+++ b/launcher/ui/dialogs/ExportToModListDialog.cpp
@@ -0,0 +1,223 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "ExportToModListDialog.h"
+#include <QCheckBox>
+#include <QComboBox>
+#include <QTextEdit>
+#include "FileSystem.h"
+#include "Markdown.h"
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/mod/ModFolderModel.h"
+#include "modplatform/helpers/ExportToModList.h"
+#include "ui_ExportToModListDialog.h"
+
+#include <QFileDialog>
+#include <QFileSystemModel>
+#include <QJsonDocument>
+#include <QMessageBox>
+#include <QPushButton>
+
+const QHash<ExportToModList::Formats, QString> ExportToModListDialog::exampleLines = {
+ { ExportToModList::HTML, "<li><a href=\"{url}\">{name}</a> [{version}] by {authors}</li>" },
+ { ExportToModList::MARKDOWN, "[{name}]({url}) [{version}] by {authors}" },
+ { ExportToModList::PLAINTXT, "{name} ({url}) [{version}] by {authors}" },
+ { ExportToModList::JSON, "{\"name\":\"{name}\",\"url\":\"{url}\",\"version\":\"{version}\",\"authors\":\"{authors}\"}," },
+ { ExportToModList::CSV, "{name},{url},{version},\"{authors}\"" },
+};
+
+ExportToModListDialog::ExportToModListDialog(InstancePtr instance, QWidget* parent)
+ : QDialog(parent), m_template_changed(false), name(instance->name()), ui(new Ui::ExportToModListDialog)
+{
+ ui->setupUi(this);
+ enableCustom(false);
+
+ MinecraftInstance* mcInstance = dynamic_cast<MinecraftInstance*>(instance.get());
+ if (mcInstance) {
+ mcInstance->loaderModList()->update();
+ connect(mcInstance->loaderModList().get(), &ModFolderModel::updateFinished, this, [this, mcInstance]() {
+ m_allMods = mcInstance->loaderModList()->allMods();
+ triggerImp();
+ });
+ }
+
+ connect(ui->formatComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &ExportToModListDialog::formatChanged);
+ connect(ui->authorsCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger);
+ connect(ui->versionCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger);
+ connect(ui->urlCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger);
+ connect(ui->authorsButton, &QPushButton::clicked, this, [this](bool) { addExtra(ExportToModList::Authors); });
+ connect(ui->versionButton, &QPushButton::clicked, this, [this](bool) { addExtra(ExportToModList::Version); });
+ connect(ui->urlButton, &QPushButton::clicked, this, [this](bool) { addExtra(ExportToModList::Url); });
+ connect(ui->templateText, &QTextEdit::textChanged, this, [this] {
+ if (ui->templateText->toPlainText() != exampleLines[format])
+ ui->formatComboBox->setCurrentIndex(5);
+ else
+ triggerImp();
+ });
+ connect(ui->copyButton, &QPushButton::clicked, this, [this](bool) {
+ this->ui->finalText->selectAll();
+ this->ui->finalText->copy();
+ });
+}
+
+ExportToModListDialog::~ExportToModListDialog()
+{
+ delete ui;
+}
+
+void ExportToModListDialog::formatChanged(int index)
+{
+ switch (index) {
+ case 0: {
+ enableCustom(false);
+ ui->resultText->show();
+ format = ExportToModList::HTML;
+ break;
+ }
+ case 1: {
+ enableCustom(false);
+ ui->resultText->show();
+ format = ExportToModList::MARKDOWN;
+ break;
+ }
+ case 2: {
+ enableCustom(false);
+ ui->resultText->hide();
+ format = ExportToModList::PLAINTXT;
+ break;
+ }
+ case 3: {
+ enableCustom(false);
+ ui->resultText->hide();
+ format = ExportToModList::JSON;
+ break;
+ }
+ case 4: {
+ enableCustom(false);
+ ui->resultText->hide();
+ format = ExportToModList::CSV;
+ break;
+ }
+ case 5: {
+ m_template_changed = true;
+ enableCustom(true);
+ ui->resultText->hide();
+ format = ExportToModList::CUSTOM;
+ break;
+ }
+ }
+ triggerImp();
+}
+
+void ExportToModListDialog::triggerImp()
+{
+ if (format == ExportToModList::CUSTOM) {
+ ui->finalText->setPlainText(ExportToModList::exportToModList(m_allMods, ui->templateText->toPlainText()));
+ return;
+ }
+ auto opt = 0;
+ if (ui->authorsCheckBox->isChecked())
+ opt |= ExportToModList::Authors;
+ if (ui->versionCheckBox->isChecked())
+ opt |= ExportToModList::Version;
+ if (ui->urlCheckBox->isChecked())
+ opt |= ExportToModList::Url;
+ auto txt = ExportToModList::exportToModList(m_allMods, format, static_cast<ExportToModList::OptionalData>(opt));
+ ui->finalText->setPlainText(txt);
+ switch (format) {
+ case ExportToModList::CUSTOM:
+ return;
+ case ExportToModList::HTML:
+ ui->resultText->setHtml(txt);
+ break;
+ case ExportToModList::MARKDOWN:
+ ui->resultText->setHtml(markdownToHTML(txt));
+ break;
+ case ExportToModList::PLAINTXT:
+ break;
+ case ExportToModList::JSON:
+ break;
+ case ExportToModList::CSV:
+ break;
+ }
+ auto exampleLine = exampleLines[format];
+ if (!m_template_changed && ui->templateText->toPlainText() != exampleLine)
+ ui->templateText->setPlainText(exampleLine);
+}
+
+void ExportToModListDialog::done(int result)
+{
+ if (result == Accepted) {
+ const QString filename = FS::RemoveInvalidFilenameChars(name);
+ const QString output =
+ QFileDialog::getSaveFileName(this, tr("Export %1").arg(name), FS::PathCombine(QDir::homePath(), filename + extension()),
+ "File (*.txt *.html *.md *.json *.csv)", nullptr);
+
+ if (output.isEmpty())
+ return;
+ FS::write(output, ui->finalText->toPlainText().toUtf8());
+ }
+
+ QDialog::done(result);
+}
+
+QString ExportToModListDialog::extension()
+{
+ switch (format) {
+ case ExportToModList::HTML:
+ return ".html";
+ case ExportToModList::MARKDOWN:
+ return ".md";
+ case ExportToModList::PLAINTXT:
+ return ".txt";
+ case ExportToModList::CUSTOM:
+ return ".txt";
+ case ExportToModList::JSON:
+ return ".json";
+ case ExportToModList::CSV:
+ return ".csv";
+ }
+ return ".txt";
+}
+
+void ExportToModListDialog::addExtra(ExportToModList::OptionalData option)
+{
+ if (format != ExportToModList::CUSTOM)
+ return;
+ switch (option) {
+ case ExportToModList::Authors:
+ ui->templateText->insertPlainText("{authors}");
+ break;
+ case ExportToModList::Url:
+ ui->templateText->insertPlainText("{url}");
+ break;
+ case ExportToModList::Version:
+ ui->templateText->insertPlainText("{version}");
+ break;
+ }
+}
+void ExportToModListDialog::enableCustom(bool enabled)
+{
+ ui->authorsCheckBox->setHidden(enabled);
+ ui->versionCheckBox->setHidden(enabled);
+ ui->urlCheckBox->setHidden(enabled);
+
+ ui->authorsButton->setHidden(!enabled);
+ ui->versionButton->setHidden(!enabled);
+ ui->urlButton->setHidden(!enabled);
+}
diff --git a/launcher/ui/dialogs/ExportToModListDialog.h b/launcher/ui/dialogs/ExportToModListDialog.h
new file mode 100644
index 00000000..9886ae5a
--- /dev/null
+++ b/launcher/ui/dialogs/ExportToModListDialog.h
@@ -0,0 +1,55 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <QDialog>
+#include <QList>
+#include "BaseInstance.h"
+#include "minecraft/mod/Mod.h"
+#include "modplatform/helpers/ExportToModList.h"
+
+namespace Ui {
+class ExportToModListDialog;
+}
+
+class ExportToModListDialog : public QDialog {
+ Q_OBJECT
+
+ public:
+ explicit ExportToModListDialog(InstancePtr instance, QWidget* parent = nullptr);
+ ~ExportToModListDialog();
+
+ void done(int result) override;
+
+ protected slots:
+ void formatChanged(int index);
+ void triggerImp();
+ void trigger(int) { triggerImp(); };
+ void addExtra(ExportToModList::OptionalData option);
+
+ private:
+ QString extension();
+ void enableCustom(bool enabled);
+ QList<Mod*> m_allMods;
+ bool m_template_changed;
+ QString name;
+ ExportToModList::Formats format = ExportToModList::Formats::HTML;
+ Ui::ExportToModListDialog* ui;
+ static const QHash<ExportToModList::Formats, QString> exampleLines;
+};
diff --git a/launcher/ui/dialogs/ExportToModListDialog.ui b/launcher/ui/dialogs/ExportToModListDialog.ui
new file mode 100644
index 00000000..25eb4342
--- /dev/null
+++ b/launcher/ui/dialogs/ExportToModListDialog.ui
@@ -0,0 +1,240 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ExportToModListDialog</class>
+ <widget class="QDialog" name="ExportToModListDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>650</width>
+ <height>446</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Export Pack to ModList</string>
+ </property>
+ <property name="sizeGripEnabled">
+ <bool>true</bool>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,0">
+ <item>
+ <widget class="QGroupBox" name="groupBox_3">
+ <property name="title">
+ <string>Settings</string>
+ </property>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="0" column="1">
+ <widget class="QComboBox" name="formatComboBox">
+ <item>
+ <property name="text">
+ <string>HTML</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Markdown</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Plaintext</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>JSON</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>CSV</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Custom</string>
+ </property>
+ </item>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QGroupBox" name="templateGroup">
+ <property name="title">
+ <string>Template</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_4">
+ <item>
+ <widget class="QTextEdit" name="templateText"/>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QGroupBox" name="optionsGroup">
+ <property name="title">
+ <string>Optional Info</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_5">
+ <item>
+ <widget class="QCheckBox" name="versionCheckBox">
+ <property name="text">
+ <string>Version</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="authorsCheckBox">
+ <property name="text">
+ <string>Authors</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="urlCheckBox">
+ <property name="text">
+ <string>URL</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="versionButton">
+ <property name="text">
+ <string>Version</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="authorsButton">
+ <property name="text">
+ <string>Authors</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="urlButton">
+ <property name="text">
+ <string>URL</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <widget class="QLabel" name="label">
+ <property name="frameShape">
+ <enum>QFrame::NoFrame</enum>
+ </property>
+ <property name="frameShadow">
+ <enum>QFrame::Plain</enum>
+ </property>
+ <property name="lineWidth">
+ <number>1</number>
+ </property>
+ <property name="text">
+ <string>Format</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="groupBox_4">
+ <property name="title">
+ <string>Result</string>
+ </property>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <widget class="QPlainTextEdit" name="finalText">
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>143</height>
+ </size>
+ </property>
+ <property name="readOnly">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QTextBrowser" name="resultText">
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="warningLabel">
+ <property name="text">
+ <string>This depends on the mods' metadata. To ensure it is available, run an update on the instance. Installing the updates isn't necessary.</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <item>
+ <widget class="QPushButton" name="copyButton">
+ <property name="text">
+ <string>Copy</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Save</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>accepted()</signal>
+ <receiver>ExportToModListDialog</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>334</x>
+ <y>435</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>324</x>
+ <y>206</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>rejected()</signal>
+ <receiver>ExportToModListDialog</receiver>
+ <slot>reject()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>324</x>
+ <y>390</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>324</x>
+ <y>206</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>
diff --git a/launcher/ui/dialogs/NewInstanceDialog.cpp b/launcher/ui/dialogs/NewInstanceDialog.cpp
index df182f09..7b9bb944 100644
--- a/launcher/ui/dialogs/NewInstanceDialog.cpp
+++ b/launcher/ui/dialogs/NewInstanceDialog.cpp
@@ -54,9 +54,8 @@
#include <utility>
#include "ui/widgets/PageContainer.h"
-#include "ui/pages/modplatform/VanillaPage.h"
+#include "ui/pages/modplatform/CustomPage.h"
#include "ui/pages/modplatform/atlauncher/AtlPage.h"
-#include "ui/pages/modplatform/ftb/FtbPage.h"
#include "ui/pages/modplatform/legacy_ftb/Page.h"
#include "ui/pages/modplatform/flame/FlamePage.h"
#include "ui/pages/modplatform/ImportPage.h"
@@ -100,7 +99,7 @@ NewInstanceDialog::NewInstanceDialog(const QString & initialGroup, const QString
// 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 = new PageContainer(this, {}, this);
m_container->setSizePolicy(QSizePolicy::Policy::Preferred, QSizePolicy::Policy::Expanding);
m_container->layout()->setContentsMargins(0, 0, 0, 0);
ui->verticalLayout->insertWidget(2, m_container);
@@ -163,12 +162,11 @@ QList<BasePage *> NewInstanceDialog::getPages()
importPage = new ImportPage(this);
- pages.append(new VanillaPage(this));
+ pages.append(new CustomPage(this));
pages.append(importPage);
pages.append(new AtlPage(this));
if (APPLICATION->capabilities() & Application::SupportsFlame)
pages.append(new FlamePage(this));
- pages.append(new FtbPage(this));
pages.append(new LegacyFTB::Page(this));
pages.append(new ModrinthPage(this));
pages.append(new TechnicPage(this));
diff --git a/launcher/ui/dialogs/NewsDialog.cpp b/launcher/ui/dialogs/NewsDialog.cpp
index e1b5dd74..b646e391 100644
--- a/launcher/ui/dialogs/NewsDialog.cpp
+++ b/launcher/ui/dialogs/NewsDialog.cpp
@@ -32,7 +32,7 @@ NewsDialog::~NewsDialog()
void NewsDialog::selectedArticleChanged(const QString& new_title)
{
- auto const& article_entry = m_entries.constFind(new_title).value();
+ auto article_entry = m_entries.constFind(new_title).value();
ui->articleTitleLabel->setText(QString("<a href='%1'>%2</a>").arg(article_entry->link, new_title));
diff --git a/launcher/ui/dialogs/ProgressDialog.cpp b/launcher/ui/dialogs/ProgressDialog.cpp
index da73a449..4243e291 100644
--- a/launcher/ui/dialogs/ProgressDialog.cpp
+++ b/launcher/ui/dialogs/ProgressDialog.cpp
@@ -1,33 +1,75 @@
-/* Copyright 2013-2021 MultiMC Contributors
+/// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * PrismLaucher - Minecraft Launcher
+ * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com>
*
- * 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
+ * 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.
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
*
- * Unless required by applicable law or agreed to in writing, software
- * 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.
+ * 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 "ProgressDialog.h"
+#include <QPoint>
#include "ui_ProgressDialog.h"
+#include <limits>
#include <QDebug>
#include <QKeyEvent>
#include "tasks/Task.h"
+#include "ui/widgets/SubTaskProgressBar.h"
+
+
+// map a value in a numeric range of an arbitrary type to between 0 and INT_MAX
+// for getting the best precision out of the qt progress bar
+template<typename T, std::enable_if_t<std::is_arithmetic_v<T>, bool> = true>
+std::tuple<int, int> map_int_zero_max(T current, T range_max, T range_min)
+{
+ int int_max = std::numeric_limits<int>::max();
+
+ auto type_range = range_max - range_min;
+ double percentage = static_cast<double>(current - range_min) / static_cast<double>(type_range);
+ int mapped_current = percentage * int_max;
+
+ return {mapped_current, int_max};
+}
+
+
ProgressDialog::ProgressDialog(QWidget* parent) : QDialog(parent), ui(new Ui::ProgressDialog)
{
ui->setupUi(this);
+ ui->taskProgressScrollArea->setHidden(true);
this->setWindowFlags(this->windowFlags() & ~Qt::WindowContextHelpButtonHint);
setAttribute(Qt::WidgetAttribute::WA_QuitOnClose, true);
- setSkipButton(false);
changeProgress(0, 100);
+ updateSize(true);
+ setSkipButton(false);
}
void ProgressDialog::setSkipButton(bool present, QString label)
@@ -53,11 +95,39 @@ ProgressDialog::~ProgressDialog()
delete ui;
}
-void ProgressDialog::updateSize()
-{
- QSize qSize = QSize(480, minimumSizeHint().height());
- resize(qSize);
- setFixedSize(qSize);
+void ProgressDialog::updateSize(bool recenterParent)
+{
+ QSize lastSize = this->size();
+ QPoint lastPos = this->pos();
+ int minHeight = ui->globalStatusDetailsLabel->minimumSize().height() + (ui->verticalLayout->spacing() * 2);
+ minHeight += ui->globalProgressBar->minimumSize().height() + ui->verticalLayout->spacing();
+ if (!ui->taskProgressScrollArea->isHidden())
+ minHeight += ui->taskProgressScrollArea->minimumSizeHint().height() + ui->verticalLayout->spacing();
+ if (ui->skipButton->isVisible())
+ minHeight += ui->skipButton->height() + ui->verticalLayout->spacing();
+ minHeight = std::max(minHeight, 60);
+ QSize minSize = QSize(480, minHeight);
+
+ setMinimumSize(minSize);
+ adjustSize();
+
+ QSize newSize = this->size();
+ // if the current window is a different size
+ auto parent = this->parentWidget();
+ if (recenterParent && parent) {
+ auto newX = std::max(0, parent->x() + ((parent->width() - newSize.width()) / 2));
+ auto newY = std::max(0, parent->y() + ((parent->height() - newSize.height()) / 2));
+ this->move(newX, newY);
+ }
+ else if (lastSize != newSize)
+ {
+ // center on old position after resize
+ QSize sizeDiff = lastSize - newSize; // last size was smaller, the results should be negative
+ auto newX = std::max(0, lastPos.x() + (sizeDiff.width() / 2));
+ auto newY = std::max(0, lastPos.y() + (sizeDiff.height() / 2));
+ this->move(newX, newY);
+ }
+
}
int ProgressDialog::execWithTask(Task* task)
@@ -79,17 +149,15 @@ int ProgressDialog::execWithTask(Task* task)
connect(task, &Task::failed, this, &ProgressDialog::onTaskFailed);
connect(task, &Task::succeeded, this, &ProgressDialog::onTaskSucceeded);
connect(task, &Task::status, this, &ProgressDialog::changeStatus);
- connect(task, &Task::stepStatus, this, &ProgressDialog::changeStatus);
+ connect(task, &Task::details, this, &ProgressDialog::changeStatus);
+ connect(task, &Task::stepProgress, this, &ProgressDialog::changeStepProgress);
connect(task, &Task::progress, this, &ProgressDialog::changeProgress);
-
connect(task, &Task::aborted, this, &ProgressDialog::hide);
connect(task, &Task::abortStatusChanged, ui->skipButton, &QPushButton::setEnabled);
m_is_multi_step = task->isMultiStep();
- if (!m_is_multi_step) {
- ui->globalStatusLabel->setHidden(true);
- ui->globalProgressBar->setHidden(true);
- }
+ ui->taskProgressScrollArea->setHidden(!m_is_multi_step);
+ updateSize();
// It's a good idea to start the task after we entered the dialog's event loop :^)
if (!task->isRunning()) {
@@ -149,23 +217,55 @@ void ProgressDialog::onTaskSucceeded()
void ProgressDialog::changeStatus(const QString& status)
{
ui->globalStatusLabel->setText(task->getStatus());
- ui->statusLabel->setText(task->getStepStatus());
+ ui->globalStatusLabel->adjustSize();
+ ui->globalStatusDetailsLabel->setText(task->getDetails());
+ ui->globalStatusDetailsLabel->adjustSize();
updateSize();
}
+void ProgressDialog::addTaskProgress(TaskStepProgress const& progress)
+{
+ SubTaskProgressBar* task_bar = new SubTaskProgressBar(this);
+ taskProgress.insert(progress.uid, task_bar);
+ ui->taskProgressLayout->addWidget(task_bar);
+}
+
+void ProgressDialog::changeStepProgress(TaskStepProgress const& task_progress)
+{
+ m_is_multi_step = true;
+ if(ui->taskProgressScrollArea->isHidden()) {
+ ui->taskProgressScrollArea->setHidden(false);
+ updateSize();
+ }
+
+ if (!taskProgress.contains(task_progress.uid))
+ addTaskProgress(task_progress);
+ auto task_bar = taskProgress.value(task_progress.uid);
+
+
+ auto const [mapped_current, mapped_total] = map_int_zero_max<qint64>(task_progress.current, task_progress.total, 0);
+ if (task_progress.total <= 0) {
+ task_bar->setRange(0, 0);
+ } else {
+ task_bar->setRange(0, mapped_total);
+ }
+
+ task_bar->setValue(mapped_current);
+ task_bar->setStatus(task_progress.status);
+ task_bar->setDetails(task_progress.details);
+
+ if (task_progress.isDone()) {
+ task_bar->setVisible(false);
+ }
+
+}
+
void ProgressDialog::changeProgress(qint64 current, qint64 total)
{
ui->globalProgressBar->setMaximum(total);
ui->globalProgressBar->setValue(current);
- if (!m_is_multi_step) {
- ui->taskProgressBar->setMaximum(total);
- ui->taskProgressBar->setValue(current);
- } else {
- ui->taskProgressBar->setMaximum(task->getStepProgress());
- ui->taskProgressBar->setValue(task->getStepTotalProgress());
- }
}
void ProgressDialog::keyPressEvent(QKeyEvent* e)
diff --git a/launcher/ui/dialogs/ProgressDialog.h b/launcher/ui/dialogs/ProgressDialog.h
index 0b4b78a4..f062be08 100644
--- a/launcher/ui/dialogs/ProgressDialog.h
+++ b/launcher/ui/dialogs/ProgressDialog.h
@@ -1,22 +1,50 @@
-/* Copyright 2013-2021 MultiMC Contributors
+/// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * PrismLaucher - Minecraft Launcher
+ * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com>
*
- * 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
+ * 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.
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
*
- * Unless required by applicable law or agreed to in writing, software
- * 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.
+ * 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 <QDialog>
#include <memory>
+#include <QHash>
+#include <QUuid>
+
+#include "QObjectPtr.h"
+#include "tasks/Task.h"
+
+#include "ui/widgets/SubTaskProgressBar.h"
class Task;
class SequentialTask;
@@ -34,7 +62,7 @@ public:
explicit ProgressDialog(QWidget *parent = 0);
~ProgressDialog();
- void updateSize();
+ void updateSize(bool recenterParent = false);
int execWithTask(Task* task);
int execWithTask(std::unique_ptr<Task> &&task);
@@ -52,6 +80,7 @@ slots:
void changeStatus(const QString &status);
void changeProgress(qint64 current, qint64 total);
+ void changeStepProgress(TaskStepProgress const& task_progress);
private
@@ -64,6 +93,7 @@ protected:
private:
bool handleImmediateResult(QDialog::DialogCode &result);
+ void addTaskProgress(TaskStepProgress const& progress);
private:
Ui::ProgressDialog *ui;
@@ -71,4 +101,8 @@ private:
Task *task;
bool m_is_multi_step = false;
+ QHash<QUuid, SubTaskProgressBar*> taskProgress;
+
+
};
+
diff --git a/launcher/ui/dialogs/ProgressDialog.ui b/launcher/ui/dialogs/ProgressDialog.ui
index 34ab71e3..a4d08124 100644
--- a/launcher/ui/dialogs/ProgressDialog.ui
+++ b/launcher/ui/dialogs/ProgressDialog.ui
@@ -2,75 +2,135 @@
<ui version="4.0">
<class>ProgressDialog</class>
<widget class="QDialog" name="ProgressDialog">
- <property name="minimumSize">
- <size>
- <width>400</width>
- <height>0</height>
- </size>
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>480</width>
+ <height>210</height>
+ </rect>
</property>
- <property name="maximumSize">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+ <horstretch>1</horstretch>
+ <verstretch>1</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
<size>
- <width>600</width>
- <height>16777215</height>
+ <width>480</width>
+ <height>210</height>
</size>
</property>
<property name="windowTitle">
<string>Please wait...</string>
</property>
- <layout class="QGridLayout" name="gridLayout">
- <item row="4" column="0">
- <widget class="QPushButton" name="skipButton">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
+ <property name="sizeGripEnabled">
+ <bool>true</bool>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,0,0">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout" stretch="1,0">
+ <item>
+ <widget class="QLabel" name="globalStatusLabel">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>15</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>Global Task Status...</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="globalStatusDetailsLabel">
+ <property name="text">
+ <string>Global Status Details...</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QProgressBar" name="globalProgressBar">
+ <property name="enabled">
+ <bool>true</bool>
</property>
- <property name="text">
- <string>Skip</string>
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>24</height>
+ </size>
</property>
- </widget>
- </item>
- <item row="0" column="0">
- <widget class="QLabel" name="globalStatusLabel">
- <property name="text">
- <string>Global Task Status...</string>
+ <property name="value">
+ <number>24</number>
</property>
</widget>
</item>
- <item row="2" column="0">
- <widget class="QLabel" name="statusLabel">
+ <item>
+ <widget class="QScrollArea" name="taskProgressScrollArea">
<property name="sizePolicy">
- <sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding">
+ <sizepolicy hsizetype="Expanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
- <property name="text">
- <string>Task Status...</string>
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>100</height>
+ </size>
</property>
- <property name="wordWrap">
- <bool>true</bool>
+ <property name="frameShape">
+ <enum>QFrame::StyledPanel</enum>
</property>
- </widget>
- </item>
- <item row="3" column="0">
- <widget class="QProgressBar" name="taskProgressBar">
- <property name="value">
- <number>24</number>
+ <property name="horizontalScrollBarPolicy">
+ <enum>Qt::ScrollBarAsNeeded</enum>
+ </property>
+ <property name="sizeAdjustPolicy">
+ <enum>QAbstractScrollArea::AdjustToContents</enum>
</property>
- <property name="textVisible">
- <bool>false</bool>
+ <property name="widgetResizable">
+ <bool>true</bool>
</property>
+ <widget class="QWidget" name="taskProgressContainer">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>464</width>
+ <height>96</height>
+ </rect>
+ </property>
+ <layout class="QVBoxLayout" name="taskProgressLayout">
+ <property name="spacing">
+ <number>2</number>
+ </property>
+ </layout>
+ </widget>
</widget>
</item>
- <item row="1" column="0">
- <widget class="QProgressBar" name="globalProgressBar">
- <property name="enabled">
- <bool>true</bool>
+ <item>
+ <widget class="QPushButton" name="skipButton">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
</property>
- <property name="value">
- <number>24</number>
+ <property name="text">
+ <string>Skip</string>
</property>
</widget>
</item>
diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.cpp b/launcher/ui/dialogs/ResourceDownloadDialog.cpp
index edb7d063..b17eced3 100644
--- a/launcher/ui/dialogs/ResourceDownloadDialog.cpp
+++ b/launcher/ui/dialogs/ResourceDownloadDialog.cpp
@@ -18,17 +18,24 @@
*/
#include "ResourceDownloadDialog.h"
+#include <QEventLoop>
+#include <QList>
#include <QPushButton>
+#include <algorithm>
#include "Application.h"
#include "ResourceDownloadTask.h"
#include "minecraft/mod/ModFolderModel.h"
#include "minecraft/mod/ResourcePackFolderModel.h"
-#include "minecraft/mod/TexturePackFolderModel.h"
#include "minecraft/mod/ShaderPackFolderModel.h"
+#include "minecraft/mod/TexturePackFolderModel.h"
+#include "minecraft/mod/tasks/GetModDependenciesTask.h"
+#include "modplatform/ModIndex.h"
+#include "ui/dialogs/CustomMessageBox.h"
+#include "ui/dialogs/ProgressDialog.h"
#include "ui/dialogs/ReviewMessageBox.h"
#include "ui/pages/modplatform/ResourcePage.h"
@@ -36,12 +43,17 @@
#include "ui/pages/modplatform/flame/FlameResourcePages.h"
#include "ui/pages/modplatform/modrinth/ModrinthResourcePages.h"
+#include "modplatform/flame/FlameAPI.h"
+#include "modplatform/modrinth/ModrinthAPI.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)
+ : QDialog(parent)
+ , m_base_model(base_model)
+ , m_buttons(QDialogButtonBox::Help | QDialogButtonBox::Ok | QDialogButtonBox::Cancel)
+ , m_vertical_layout(this)
{
setObjectName(QStringLiteral("ResourceDownloadDialog"));
@@ -89,7 +101,7 @@ void ResourceDownloadDialog::reject()
// won't work with subclasses if we put it in this ctor.
void ResourceDownloadDialog::initializeContainer()
{
- m_container = new PageContainer(this);
+ m_container = new PageContainer(this, {}, this);
m_container->setSizePolicy(QSizePolicy::Policy::Preferred, QSizePolicy::Policy::Expanding);
m_container->layout()->setContentsMargins(0, 0, 0, 0);
m_vertical_layout.addWidget(m_container);
@@ -102,7 +114,8 @@ void ResourceDownloadDialog::initializeContainer()
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()));
+ 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);
@@ -112,23 +125,79 @@ void ResourceDownloadDialog::connectButtons()
connect(HelpButton, &QPushButton::clicked, m_container, &PageContainer::help);
}
-void ResourceDownloadDialog::confirm()
+static ModPlatform::ProviderCapabilities ProviderCaps;
+
+QStringList getRequiredBy(QList<ResourceDownloadDialog::DownloadTaskPtr> tasks, ResourceDownloadDialog::DownloadTaskPtr pack)
{
- auto keys = m_selected.keys();
- keys.sort(Qt::CaseInsensitive);
+ auto addonId = pack->getPack()->addonId;
+ auto provider = pack->getPack()->provider;
+ auto version = pack->getVersionID();
+ auto req = QStringList();
+ for (auto& task : tasks) {
+ if (provider != task->getPack()->provider)
+ continue;
+ auto deps = task->getVersion().dependencies;
+ if (auto dep = std::find_if(deps.begin(), deps.end(),
+ [addonId, provider, version](const ModPlatform::Dependency& d) {
+ return d.type == ModPlatform::DependencyType::REQUIRED &&
+ (provider == ModPlatform::ResourceProvider::MODRINTH && d.addonId.toString().isEmpty()
+ ? version == d.version
+ : d.addonId == addonId);
+ });
+ dep != deps.end()) {
+ req.append(task->getName());
+ }
+ }
+ return req;
+}
+void ResourceDownloadDialog::confirm()
+{
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 (auto task = getModDependenciesTask(); task) {
+ connect(task.get(), &Task::failed, this,
+ [&](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); });
+
+ connect(task.get(), &Task::succeeded, this, [&]() {
+ QStringList warnings = task->warnings();
+ if (warnings.count()) {
+ CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->exec();
+ }
+ });
+
+ // Check for updates
+ ProgressDialog progress_dialog(this);
+ progress_dialog.setSkipButton(true, tr("Abort"));
+ progress_dialog.setWindowTitle(tr("Checking for dependencies..."));
+ auto ret = progress_dialog.execWithTask(task.get());
+
+ // If the dialog was skipped / some download error happened
+ if (ret == QDialog::DialogCode::Rejected) {
+ QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection);
+ return;
+ } else {
+ for (auto dep : task->getDependecies())
+ addResource(dep->pack, dep->version);
+ }
+ }
+
+ auto selected = getTasks();
+ std::sort(selected.begin(), selected.end(), [](const DownloadTaskPtr& a, const DownloadTaskPtr& b) {
+ return QString::compare(a->getName(), b->getName(), Qt::CaseInsensitive) < 0;
+ });
+ for (auto& task : selected) {
+ confirm_dialog->appendResource({ task->getName(), task->getFilename(), task->getCustomPath(),
+ ProviderCaps.name(task->getProvider()), getRequiredBy(selected, task) });
}
if (confirm_dialog->exec()) {
auto deselected = confirm_dialog->deselectedResources();
- for (auto name : deselected) {
- m_selected.remove(name);
+ for (auto page : m_container->getPages()) {
+ auto res = static_cast<ResourcePage*>(page);
+ for (auto name : deselected)
+ res->removeResourceFromPage(name);
}
this->accept();
@@ -145,46 +214,39 @@ ResourcePage* ResourceDownloadDialog::getSelectedPage()
return m_selectedPage;
}
-void ResourceDownloadDialog::addResource(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, bool is_indexed)
+void ResourceDownloadDialog::addResource(ModPlatform::IndexedPack::Ptr pack, ModPlatform::IndexedVersion& ver)
{
- 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());
+ removeResource(pack->name);
+ m_selectedPage->addResourceToPage(pack, ver, getBaseModel());
+ setButtonStatus();
}
-static ModPlatform::IndexedVersion& getVersionWithID(ModPlatform::IndexedPack& pack, QVariant id)
+void ResourceDownloadDialog::removeResource(const QString& pack_name)
{
- 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;
+ for (auto page : m_container->getPages()) {
+ static_cast<ResourcePage*>(page)->removeResourceFromPage(pack_name);
+ }
+ setButtonStatus();
}
-void ResourceDownloadDialog::removeResource(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver)
+void ResourceDownloadDialog::setButtonStatus()
{
- 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;
+ auto selected = false;
+ for (auto page : m_container->getPages()) {
+ auto res = static_cast<ResourcePage*>(page);
+ selected = selected || res->hasSelectedPacks();
}
-
- // 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());
+ m_buttons.button(QDialogButtonBox::Ok)->setEnabled(selected);
}
const QList<ResourceDownloadDialog::DownloadTaskPtr> ResourceDownloadDialog::getTasks()
{
- return m_selected.values();
+ QList<DownloadTaskPtr> selected;
+ for (auto page : m_container->getPages()) {
+ auto res = static_cast<ResourcePage*>(page);
+ selected.append(res->selectedPacks());
+ }
+ return selected;
}
void ResourceDownloadDialog::selectedPageChanged(BasePage* previous, BasePage* selected)
@@ -205,8 +267,6 @@ void ResourceDownloadDialog::selectedPageChanged(BasePage* previous, BasePage* s
m_selectedPage->setSearchTerm(prev_page->getSearchTerm());
}
-
-
ModDownloadDialog::ModDownloadDialog(QWidget* parent, const std::shared_ptr<ModFolderModel>& mods, BaseInstance* instance)
: ResourceDownloadDialog(parent, mods), m_instance(instance)
{
@@ -223,8 +283,11 @@ QList<BasePage*> ModDownloadDialog::getPages()
{
QList<BasePage*> pages;
- pages.append(ModrinthModPage::create(this, *m_instance));
- if (APPLICATION->capabilities() & Application::SupportsFlame)
+ auto loaders = static_cast<MinecraftInstance*>(m_instance)->getPackProfile()->getModLoaders().value();
+
+ if (ModrinthAPI::validateModLoaders(loaders))
+ pages.append(ModrinthModPage::create(this, *m_instance));
+ if (APPLICATION->capabilities() & Application::SupportsFlame && FlameAPI::validateModLoaders(loaders))
pages.append(FlameModPage::create(this, *m_instance));
m_selectedPage = dynamic_cast<ModPage*>(pages[0]);
@@ -232,6 +295,18 @@ QList<BasePage*> ModDownloadDialog::getPages()
return pages;
}
+GetModDependenciesTask::Ptr ModDownloadDialog::getModDependenciesTask()
+{
+ if (auto model = dynamic_cast<ModFolderModel*>(getBaseModel().get()); model) {
+ QList<std::shared_ptr<GetModDependenciesTask::PackDependency>> selectedVers;
+ for (auto& selected : getTasks()) {
+ selectedVers.append(std::make_shared<GetModDependenciesTask::PackDependency>(selected->getPack(), selected->getVersion()));
+ }
+
+ return makeShared<GetModDependenciesTask>(this, m_instance, model, selectedVers);
+ }
+ return nullptr;
+};
ResourcePackDownloadDialog::ResourcePackDownloadDialog(QWidget* parent,
const std::shared_ptr<ResourcePackFolderModel>& resource_packs,
@@ -255,10 +330,11 @@ QList<BasePage*> ResourcePackDownloadDialog::getPages()
if (APPLICATION->capabilities() & Application::SupportsFlame)
pages.append(FlameResourcePackPage::create(this, *m_instance));
+ m_selectedPage = dynamic_cast<ResourcePackResourcePage*>(pages[0]);
+
return pages;
}
-
TexturePackDownloadDialog::TexturePackDownloadDialog(QWidget* parent,
const std::shared_ptr<TexturePackFolderModel>& resource_packs,
BaseInstance* instance)
@@ -281,10 +357,11 @@ QList<BasePage*> TexturePackDownloadDialog::getPages()
if (APPLICATION->capabilities() & Application::SupportsFlame)
pages.append(FlameTexturePackPage::create(this, *m_instance));
+ m_selectedPage = dynamic_cast<TexturePackResourcePage*>(pages[0]);
+
return pages;
}
-
ShaderPackDownloadDialog::ShaderPackDownloadDialog(QWidget* parent,
const std::shared_ptr<ShaderPackFolderModel>& shaders,
BaseInstance* instance)
@@ -305,6 +382,8 @@ QList<BasePage*> ShaderPackDownloadDialog::getPages()
pages.append(ModrinthShaderPackPage::create(this, *m_instance));
+ m_selectedPage = dynamic_cast<ShaderPackResourcePage*>(pages[0]);
+
return pages;
}
diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.h b/launcher/ui/dialogs/ResourceDownloadDialog.h
index 5678dc8b..f65daaa3 100644
--- a/launcher/ui/dialogs/ResourceDownloadDialog.h
+++ b/launcher/ui/dialogs/ResourceDownloadDialog.h
@@ -25,6 +25,7 @@
#include <QLayout>
#include "QObjectPtr.h"
+#include "minecraft/mod/tasks/GetModDependenciesTask.h"
#include "modplatform/ModIndex.h"
#include "ui/pages/BasePageProvider.h"
@@ -62,8 +63,8 @@ class ResourceDownloadDialog : public QDialog, public BasePageProvider {
bool selectPage(QString pageId);
ResourcePage* getSelectedPage();
- void addResource(ModPlatform::IndexedPack&, ModPlatform::IndexedVersion&, bool is_indexed = false);
- void removeResource(ModPlatform::IndexedPack&, ModPlatform::IndexedVersion&);
+ void addResource(ModPlatform::IndexedPack::Ptr, ModPlatform::IndexedVersion&);
+ void removeResource(const QString&);
const QList<DownloadTaskPtr> getTasks();
[[nodiscard]] const std::shared_ptr<ResourceFolderModel> getBaseModel() const { return m_base_model; }
@@ -79,6 +80,9 @@ class ResourceDownloadDialog : public QDialog, public BasePageProvider {
protected:
[[nodiscard]] virtual QString geometrySaveKey() const { return ""; }
+ void setButtonStatus();
+
+ [[nodiscard]] virtual GetModDependenciesTask::Ptr getModDependenciesTask() { return nullptr; }
protected:
const std::shared_ptr<ResourceFolderModel> m_base_model;
@@ -88,12 +92,8 @@ class ResourceDownloadDialog : public QDialog, public BasePageProvider {
QDialogButtonBox m_buttons;
QVBoxLayout m_vertical_layout;
-
- QHash<QString, DownloadTaskPtr> m_selected;
};
-
-
class ModDownloadDialog final : public ResourceDownloadDialog {
Q_OBJECT
@@ -106,6 +106,7 @@ class ModDownloadDialog final : public ResourceDownloadDialog {
[[nodiscard]] QString geometrySaveKey() const override { return "ModDownloadGeometry"; }
QList<BasePage*> getPages() override;
+ GetModDependenciesTask::Ptr getModDependenciesTask() override;
private:
BaseInstance* m_instance;
@@ -135,8 +136,8 @@ class TexturePackDownloadDialog final : public ResourceDownloadDialog {
public:
explicit TexturePackDownloadDialog(QWidget* parent,
- const std::shared_ptr<TexturePackFolderModel>& resource_packs,
- BaseInstance* instance);
+ const std::shared_ptr<TexturePackFolderModel>& resource_packs,
+ BaseInstance* instance);
~TexturePackDownloadDialog() override = default;
//: String that gets appended to the texture pack download dialog title ("Download " + resourcesString())
@@ -153,9 +154,7 @@ class ShaderPackDownloadDialog final : public ResourceDownloadDialog {
Q_OBJECT
public:
- explicit ShaderPackDownloadDialog(QWidget* parent,
- const std::shared_ptr<ShaderPackFolderModel>& shader_packs,
- BaseInstance* instance);
+ explicit ShaderPackDownloadDialog(QWidget* parent, const std::shared_ptr<ShaderPackFolderModel>& shader_packs, BaseInstance* instance);
~ShaderPackDownloadDialog() override = default;
//: String that gets appended to the shader pack download dialog title ("Download " + resourcesString())
diff --git a/launcher/ui/dialogs/ReviewMessageBox.cpp b/launcher/ui/dialogs/ReviewMessageBox.cpp
index 7b2df278..7b33765f 100644
--- a/launcher/ui/dialogs/ReviewMessageBox.cpp
+++ b/launcher/ui/dialogs/ReviewMessageBox.cpp
@@ -40,7 +40,8 @@ void ReviewMessageBox::appendResource(ResourceInformation&& info)
auto filenameItem = new QTreeWidgetItem(itemTop);
filenameItem->setText(0, tr("Filename: %1").arg(info.filename));
- itemTop->insertChildren(0, { filenameItem });
+ auto childIndx = 0;
+ itemTop->insertChildren(childIndx++, { filenameItem });
if (!info.custom_file_path.isEmpty()) {
auto customPathItem = new QTreeWidgetItem(itemTop);
@@ -49,7 +50,31 @@ void ReviewMessageBox::appendResource(ResourceInformation&& info)
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."));
+ itemTop->setToolTip(
+ childIndx++,
+ tr("This file will be downloaded to a folder location different from the default, possibly due to its loader requiring it."));
+ }
+
+ auto providerItem = new QTreeWidgetItem(itemTop);
+ providerItem->setText(0, tr("Provider: %1").arg(info.provider));
+
+ itemTop->insertChildren(childIndx++, { providerItem });
+
+ if (!info.required_by.isEmpty()) {
+ auto requiredByItem = new QTreeWidgetItem(itemTop);
+ if (info.required_by.length() == 1) {
+ requiredByItem->setText(0, tr("Required by: %1").arg(info.required_by.back()));
+ } else {
+ requiredByItem->setText(0, tr("Required by:"));
+ auto i = 0;
+ for (auto req : info.required_by) {
+ auto reqItem = new QTreeWidgetItem(requiredByItem);
+ reqItem->setText(0, req);
+ reqItem->insertChildren(i++, { reqItem });
+ }
+ }
+
+ itemTop->insertChildren(childIndx++, { requiredByItem });
}
ui->modTreeWidget->addTopLevelItem(itemTop);
diff --git a/launcher/ui/dialogs/ReviewMessageBox.h b/launcher/ui/dialogs/ReviewMessageBox.h
index 5ec2bc23..a520cc2a 100644
--- a/launcher/ui/dialogs/ReviewMessageBox.h
+++ b/launcher/ui/dialogs/ReviewMessageBox.h
@@ -13,9 +13,11 @@ class ReviewMessageBox : public QDialog {
static auto create(QWidget* parent, QString&& title, QString&& icon = "") -> ReviewMessageBox*;
using ResourceInformation = struct res_info {
- QString name;
- QString filename;
- QString custom_file_path {};
+ QString name;
+ QString filename;
+ QString custom_file_path{};
+ QString provider;
+ QStringList required_by;
};
void appendResource(ResourceInformation&& info);
diff --git a/launcher/ui/dialogs/VersionSelectDialog.cpp b/launcher/ui/dialogs/VersionSelectDialog.cpp
index d7880334..5feb70d2 100644
--- a/launcher/ui/dialogs/VersionSelectDialog.cpp
+++ b/launcher/ui/dialogs/VersionSelectDialog.cpp
@@ -1,16 +1,36 @@
-/* Copyright 2013-2021 MultiMC Contributors
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
*
- * 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
+ * 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.
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
*
- * Unless required by applicable law or agreed to in writing, software
- * 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.
+ * 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 "VersionSelectDialog.h"
@@ -22,15 +42,10 @@
#include <QtWidgets/QVBoxLayout>
#include <QDebug>
-#include "ui/dialogs/ProgressDialog.h"
#include "ui/widgets/VersionSelectWidget.h"
-#include "ui/dialogs/CustomMessageBox.h"
#include "BaseVersion.h"
#include "BaseVersionList.h"
-#include "tasks/Task.h"
-#include "Application.h"
-#include "VersionProxyModel.h"
VersionSelectDialog::VersionSelectDialog(BaseVersionList *vlist, QString title, QWidget *parent, bool cancelable)
: QDialog(parent)
@@ -40,7 +55,7 @@ VersionSelectDialog::VersionSelectDialog(BaseVersionList *vlist, QString title,
m_verticalLayout = new QVBoxLayout(this);
m_verticalLayout->setObjectName(QStringLiteral("verticalLayout"));
- m_versionWidget = new VersionSelectWidget(parent);
+ m_versionWidget = new VersionSelectWidget(true, parent);
m_verticalLayout->addWidget(m_versionWidget);
m_horizontalLayout = new QHBoxLayout();
diff --git a/launcher/ui/instanceview/AccessibleInstanceView.cpp b/launcher/ui/instanceview/AccessibleInstanceView.cpp
index 7de3ac72..2e7b8300 100644
--- a/launcher/ui/instanceview/AccessibleInstanceView.cpp
+++ b/launcher/ui/instanceview/AccessibleInstanceView.cpp
@@ -248,8 +248,8 @@ bool AccessibleInstanceView::selectColumn(int column)
if (view()->selectionBehavior() != QAbstractItemView::SelectColumns && rowCount() > 1) {
return false;
}
- // fallthrough intentional
}
+ /* fallthrough */
case QAbstractItemView::ContiguousSelection: {
if ((!column || !view()->selectionModel()->isColumnSelected(column - 1, view()->rootIndex())) && !view()->selectionModel()->isColumnSelected(column + 1, view()->rootIndex())) {
view()->clearSelection();
diff --git a/launcher/ui/instanceview/InstanceView.cpp b/launcher/ui/instanceview/InstanceView.cpp
index fbeffe35..1911dd59 100644
--- a/launcher/ui/instanceview/InstanceView.cpp
+++ b/launcher/ui/instanceview/InstanceView.cpp
@@ -48,6 +48,7 @@
#include <QAccessible>
#include "VisualGroup.h"
+#include "ui/themes/ThemeManager.h"
#include <QDebug>
#include <Application.h>
@@ -73,6 +74,7 @@ InstanceView::InstanceView(QWidget *parent)
setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
setAcceptDrops(true);
setAutoScroll(true);
+ setPaintCat(APPLICATION->settings()->get("TheCat").toBool());
}
InstanceView::~InstanceView()
@@ -498,12 +500,34 @@ void InstanceView::mouseDoubleClickEvent(QMouseEvent *event)
}
}
-void InstanceView::paintEvent(QPaintEvent *event)
+void InstanceView::setPaintCat(bool visible)
+{
+ m_catVisible = visible;
+ if (visible)
+ m_catPixmap.load(QString(":/backgrounds/%1").arg(ThemeManager::getCatImage()));
+ else
+ m_catPixmap = QPixmap();
+}
+
+void InstanceView::paintEvent(QPaintEvent* event)
{
executeDelayedItemsLayout();
QPainter painter(this->viewport());
+ if (m_catVisible) {
+ int widWidth = this->viewport()->width();
+ int widHeight = this->viewport()->height();
+ if (m_catPixmap.width() < widWidth)
+ widWidth = m_catPixmap.width();
+ if (m_catPixmap.height() < widHeight)
+ widHeight = m_catPixmap.height();
+ auto pixmap = m_catPixmap.scaled(widWidth, widHeight, Qt::KeepAspectRatio);
+ QRect rectOfPixmap = pixmap.rect();
+ rectOfPixmap.moveBottomRight(this->viewport()->rect().bottomRight());
+ painter.drawPixmap(rectOfPixmap.topLeft(), pixmap);
+ }
+
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
QStyleOptionViewItem option;
initViewItemOption(&option);
diff --git a/launcher/ui/instanceview/InstanceView.h b/launcher/ui/instanceview/InstanceView.h
index ac338274..36405675 100644
--- a/launcher/ui/instanceview/InstanceView.h
+++ b/launcher/ui/instanceview/InstanceView.h
@@ -85,10 +85,8 @@ public:
virtual QRegion visualRegionForSelection(const QItemSelection &selection) const override;
- int spacing() const
- {
- return m_spacing;
- };
+ int spacing() const { return m_spacing; };
+ void setPaintCat(bool visible);
public slots:
virtual void updateGeometries() override;
@@ -139,6 +137,8 @@ private:
int m_currentItemsPerRow = -1;
int m_currentCursorColumn= -1;
mutable QCache<int, QRect> geometryCache;
+ bool m_catVisible = false;
+ QPixmap m_catPixmap;
// point where the currently active mouse action started in geometry coordinates
QPoint m_pressedPosition;
diff --git a/launcher/ui/pages/BasePage.h b/launcher/ui/pages/BasePage.h
index ceb24040..dc2bde99 100644
--- a/launcher/ui/pages/BasePage.h
+++ b/launcher/ui/pages/BasePage.h
@@ -35,15 +35,16 @@
#pragma once
-#include <QString>
#include <QIcon>
+#include <QString>
+#include <functional>
#include <memory>
#include "BasePageContainer.h"
-class BasePage
-{
-public:
+class BasePage {
+ public:
+ using updateExtraInfoFunc = std::function<void(QString, QString)>;
virtual ~BasePage() {}
virtual QString id() const = 0;
virtual QString displayName() const = 0;
@@ -63,17 +64,16 @@ public:
}
virtual void openedImpl() {}
virtual void closedImpl() {}
- virtual void setParentContainer(BasePageContainer * container)
- {
- m_container = container;
- };
- virtual void retranslate() { }
+ virtual void setParentContainer(BasePageContainer* container) { m_container = container; };
+ virtual void retranslate() {}
-public:
+ public:
int stackIndex = -1;
int listIndex = -1;
-protected:
- BasePageContainer * m_container = nullptr;
+ updateExtraInfoFunc updateExtraInfo;
+
+ protected:
+ BasePageContainer* m_container = nullptr;
bool isOpened = false;
};
diff --git a/launcher/ui/pages/global/APIPage.cpp b/launcher/ui/pages/global/APIPage.cpp
index f662ee1c..668aa007 100644
--- a/launcher/ui/pages/global/APIPage.cpp
+++ b/launcher/ui/pages/global/APIPage.cpp
@@ -81,6 +81,8 @@ APIPage::APIPage(QWidget *parent) :
connect(ui->pasteTypeComboBox, currentIndexChangedSignal, this, &APIPage::updateBaseURLPlaceholder);
// This function needs to be called even when the ComboBox's index is still in its default state.
updateBaseURLPlaceholder(ui->pasteTypeComboBox->currentIndex());
+ // NOTE: this allows http://, but we replace that with https later anyway
+ ui->metaURL->setValidator(new QRegularExpressionValidator(validUrlRegExp, ui->metaURL));
ui->baseURLEntry->setValidator(new QRegularExpressionValidator(validUrlRegExp, ui->baseURLEntry));
ui->msaClientID->setValidator(new QRegularExpressionValidator(validMSAClientID, ui->msaClientID));
ui->flameKey->setValidator(new QRegularExpressionValidator(validFlameKey, ui->flameKey));
@@ -163,7 +165,7 @@ void APIPage::applySettings()
QString msaClientID = ui->msaClientID->text();
s->set("MSAClientIDOverride", msaClientID);
- QUrl metaURL = ui->metaURL->text();
+ QUrl metaURL(ui->metaURL->text());
// Add required trailing slash
if (!metaURL.isEmpty() && !metaURL.path().endsWith('/'))
{
@@ -177,7 +179,7 @@ void APIPage::applySettings()
metaURL.setScheme("https");
}
- s->set("MetaURLOverride", metaURL);
+ s->set("MetaURLOverride", metaURL.toString());
QString flameKey = ui->flameKey->text();
s->set("FlameKeyOverride", flameKey);
QString modrinthToken = ui->modrinthToken->text();
diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui
index 55bd3eea..26408f44 100644
--- a/launcher/ui/pages/global/LauncherPage.ui
+++ b/launcher/ui/pages/global/LauncherPage.ui
@@ -169,10 +169,10 @@
<item>
<widget class="QCheckBox" name="metadataDisableBtn">
<property name="toolTip">
- <string>Disable using metadata provided by mod providers (like Modrinth or Curseforge) for mods.</string>
+ <string>Disable using metadata provided by mod providers (like Modrinth or CurseForge) for mods.</string>
</property>
<property name="text">
- <string>Disable using metadata for mods?</string>
+ <string>Disable using metadata for mods</string>
</property>
</widget>
</item>
@@ -307,21 +307,21 @@
<item>
<widget class="QCheckBox" name="showConsoleCheck">
<property name="text">
- <string>Show console while the game is &amp;running?</string>
+ <string>Show console while the game is &amp;running</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="autoCloseConsoleCheck">
<property name="text">
- <string>&amp;Automatically close console when the game quits?</string>
+ <string>&amp;Automatically close console when the game quits</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="showConsoleErrorCheck">
<property name="text">
- <string>Show console when the game &amp;crashes?</string>
+ <string>Show console when the game &amp;crashes</string>
</property>
</widget>
</item>
diff --git a/launcher/ui/pages/global/MinecraftPage.cpp b/launcher/ui/pages/global/MinecraftPage.cpp
index cc597fe0..eca3e865 100644
--- a/launcher/ui/pages/global/MinecraftPage.cpp
+++ b/launcher/ui/pages/global/MinecraftPage.cpp
@@ -46,7 +46,6 @@
MinecraftPage::MinecraftPage(QWidget *parent) : QWidget(parent), ui(new Ui::MinecraftPage)
{
ui->setupUi(this);
- ui->tabWidget->tabBar()->hide();
loadSettings();
updateCheckboxStuff();
}
diff --git a/launcher/ui/pages/global/MinecraftPage.ui b/launcher/ui/pages/global/MinecraftPage.ui
index 640f436d..8f5de725 100644
--- a/launcher/ui/pages/global/MinecraftPage.ui
+++ b/launcher/ui/pages/global/MinecraftPage.ui
@@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>936</width>
- <height>1134</height>
+ <height>541</height>
</rect>
</property>
<property name="sizePolicy">
@@ -39,7 +39,7 @@
</property>
<widget class="QWidget" name="minecraftTab">
<attribute name="title">
- <string notr="true">Minecraft</string>
+ <string notr="true">General</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
@@ -51,7 +51,7 @@
<item>
<widget class="QCheckBox" name="maximizedCheckBox">
<property name="text">
- <string>Start Minecraft &amp;maximized?</string>
+ <string>Start Minecraft &amp;maximized</string>
</property>
</widget>
</item>
@@ -112,22 +112,29 @@
</widget>
</item>
<item>
- <widget class="QGroupBox" name="nativeLibWorkaroundGroupBox">
+ <widget class="QGroupBox" name="gameTimeGroupBox">
<property name="title">
- <string>Native library workarounds</string>
+ <string>Game time</string>
</property>
- <layout class="QVBoxLayout" name="verticalLayout_5">
+ <layout class="QVBoxLayout" name="verticalLayout_6">
<item>
- <widget class="QCheckBox" name="useNativeGLFWCheck">
+ <widget class="QCheckBox" name="showGameTime">
<property name="text">
- <string>Use system installation of &amp;GLFW</string>
+ <string>Show time spent &amp;playing instances</string>
</property>
</widget>
</item>
<item>
- <widget class="QCheckBox" name="useNativeOpenALCheck">
+ <widget class="QCheckBox" name="showGlobalGameTime">
<property name="text">
- <string>Use system installation of &amp;OpenAL</string>
+ <string>Show time spent playing across &amp;all instances</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="recordGameTime">
+ <property name="text">
+ <string>&amp;Record time spent playing instances</string>
</property>
</widget>
</item>
@@ -135,38 +142,28 @@
</widget>
</item>
<item>
- <widget class="QGroupBox" name="perfomanceGroupBox">
+ <widget class="QGroupBox" name="groupBox">
<property name="title">
- <string>Performance</string>
+ <string>Miscellaneous</string>
</property>
- <layout class="QVBoxLayout" name="verticalLayout_2">
- <item>
- <widget class="QCheckBox" name="enableFeralGamemodeCheck">
- <property name="toolTip">
- <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Enable Feral Interactive's GameMode, to potentially improve gaming performance.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
- </property>
- <property name="text">
- <string>Enable Feral GameMode</string>
- </property>
- </widget>
- </item>
+ <layout class="QVBoxLayout" name="verticalLayout">
<item>
- <widget class="QCheckBox" name="enableMangoHud">
+ <widget class="QCheckBox" name="closeAfterLaunchCheck">
<property name="toolTip">
- <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Enable MangoHud's advanced performance overlay.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The launcher will automatically reopen when the game crashes or exits.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
- <string>Enable MangoHud</string>
+ <string>&amp;Close the launcher after game window opens</string>
</property>
</widget>
</item>
<item>
- <widget class="QCheckBox" name="useDiscreteGpuCheck">
+ <widget class="QCheckBox" name="quitAfterGameStopCheck">
<property name="toolTip">
- <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Use the discrete GPU instead of the primary GPU.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The launcher will automatically quit after the game exits or crashes.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
- <string>Use discrete GPU</string>
+ <string>&amp;Quit the launcher after game window closes</string>
</property>
</widget>
</item>
@@ -174,29 +171,42 @@
</widget>
</item>
<item>
- <widget class="QGroupBox" name="gameTimeGroupBox">
+ <spacer name="verticalSpacerMinecraft">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="tab">
+ <attribute name="title">
+ <string>Tweaks</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_12">
+ <item>
+ <widget class="QGroupBox" name="nativeLibWorkaroundGroupBox">
<property name="title">
- <string>Game time</string>
+ <string>Native library workarounds</string>
</property>
- <layout class="QVBoxLayout" name="verticalLayout_6">
+ <layout class="QVBoxLayout" name="verticalLayout_11">
<item>
- <widget class="QCheckBox" name="showGameTime">
- <property name="text">
- <string>Show time spent &amp;playing instances</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QCheckBox" name="showGlobalGameTime">
+ <widget class="QCheckBox" name="useNativeGLFWCheck">
<property name="text">
- <string>Show time spent playing across &amp;all instances</string>
+ <string>Use system installation of &amp;GLFW</string>
</property>
</widget>
</item>
<item>
- <widget class="QCheckBox" name="recordGameTime">
+ <widget class="QCheckBox" name="useNativeOpenALCheck">
<property name="text">
- <string>&amp;Record time spent playing instances</string>
+ <string>Use system installation of &amp;OpenAL</string>
</property>
</widget>
</item>
@@ -204,28 +214,38 @@
</widget>
</item>
<item>
- <widget class="QGroupBox" name="groupBox">
+ <widget class="QGroupBox" name="perfomanceGroupBox">
<property name="title">
- <string>Miscellaneous</string>
+ <string>Performance</string>
</property>
- <layout class="QVBoxLayout" name="verticalLayout">
+ <layout class="QVBoxLayout" name="verticalLayout_2">
<item>
- <widget class="QCheckBox" name="closeAfterLaunchCheck">
+ <widget class="QCheckBox" name="enableFeralGamemodeCheck">
<property name="toolTip">
- <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The launcher will automatically reopen when the game crashes or exits.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Enable Feral Interactive's GameMode, to potentially improve gaming performance.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
- <string>&amp;Close the launcher after game window opens</string>
+ <string>Enable Feral GameMode</string>
</property>
</widget>
</item>
<item>
- <widget class="QCheckBox" name="quitAfterGameStopCheck">
+ <widget class="QCheckBox" name="enableMangoHud">
<property name="toolTip">
- <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The launcher will automatically quit after the game exits or crashes.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Enable MangoHud's advanced performance overlay.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
- <string>&amp;Quit the launcher after game window closes</string>
+ <string>Enable MangoHud</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="useDiscreteGpuCheck">
+ <property name="toolTip">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Use the discrete GPU instead of the primary GPU.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="text">
+ <string>Use discrete GPU</string>
</property>
</widget>
</item>
@@ -233,14 +253,14 @@
</widget>
</item>
<item>
- <spacer name="verticalSpacerMinecraft">
+ <spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
- <width>0</width>
- <height>0</height>
+ <width>20</width>
+ <height>40</height>
</size>
</property>
</spacer>
@@ -255,11 +275,6 @@
<tabstop>maximizedCheckBox</tabstop>
<tabstop>windowWidthSpinBox</tabstop>
<tabstop>windowHeightSpinBox</tabstop>
- <tabstop>useNativeGLFWCheck</tabstop>
- <tabstop>useNativeOpenALCheck</tabstop>
- <tabstop>enableFeralGamemodeCheck</tabstop>
- <tabstop>enableMangoHud</tabstop>
- <tabstop>useDiscreteGpuCheck</tabstop>
</tabstops>
<resources/>
<connections/>
diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.cpp b/launcher/ui/pages/instance/ExternalResourcesPage.cpp
index 1115ddc3..12038f88 100644
--- a/launcher/ui/pages/instance/ExternalResourcesPage.cpp
+++ b/launcher/ui/pages/instance/ExternalResourcesPage.cpp
@@ -1,3 +1,38 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * 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 "ExternalResourcesPage.h"
#include "ui/dialogs/CustomMessageBox.h"
#include "ui_ExternalResourcesPage.h"
@@ -9,6 +44,7 @@
#include <QKeyEvent>
#include <QMenu>
+#include <algorithm>
ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared_ptr<ResourceFolderModel> model, QWidget* parent)
: QMainWindow(parent), m_instance(instance), ui(new Ui::ExternalResourcesPage), m_model(model)
@@ -24,6 +60,8 @@ ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared
m_filterModel->setSourceModel(m_model.get());
m_filterModel->setFilterKeyColumn(-1);
ui->treeView->setModel(m_filterModel);
+ // must come after setModel
+ ui->treeView->setResizeModes(m_model->columnResizeModes());
ui->treeView->installEventFilter(this);
ui->treeView->sortByColumn(1, Qt::AscendingOrder);
@@ -43,7 +81,21 @@ ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared
auto selection_model = ui->treeView->selectionModel();
connect(selection_model, &QItemSelectionModel::currentChanged, this, &ExternalResourcesPage::current);
+ auto updateExtra = [this]() {
+ if (updateExtraInfo)
+ updateExtraInfo(id(), extraHeaderInfoString());
+ };
+ connect(selection_model, &QItemSelectionModel::selectionChanged, this, updateExtra);
+ connect(model.get(), &ResourceFolderModel::updateFinished, this, updateExtra);
+
connect(ui->filterEdit, &QLineEdit::textChanged, this, &ExternalResourcesPage::filterTextChanged);
+
+ auto viewHeader = ui->treeView->header();
+ viewHeader->setContextMenuPolicy(Qt::CustomContextMenu);
+
+ connect(viewHeader, &QHeaderView::customContextMenuRequested, this, &ExternalResourcesPage::ShowHeaderContextMenu);
+
+ m_model->loadHiddenColumns(ui->treeView);
}
ExternalResourcesPage::~ExternalResourcesPage()
@@ -65,6 +117,13 @@ void ExternalResourcesPage::ShowContextMenu(const QPoint& pos)
delete menu;
}
+void ExternalResourcesPage::ShowHeaderContextMenu(const QPoint& pos)
+{
+ auto menu = m_model->createHeaderContextMenu(ui->treeView);
+ menu->exec(ui->treeView->mapToGlobal(pos));
+ menu->deleteLater();
+}
+
void ExternalResourcesPage::openedImpl()
{
m_model->startWatching();
@@ -92,11 +151,7 @@ void ExternalResourcesPage::retranslate()
void ExternalResourcesPage::itemActivated(const QModelIndex&)
{
- if (!m_controlsEnabled)
- return;
-
auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection());
- m_model->setResourceEnabled(selection.indexes(), EnableAction::TOGGLE);
}
void ExternalResourcesPage::filterTextChanged(const QString& newContents)
@@ -139,9 +194,6 @@ bool ExternalResourcesPage::eventFilter(QObject* obj, QEvent* ev)
void ExternalResourcesPage::addItem()
{
- if (!m_controlsEnabled)
- return;
-
auto list = GuiUtil::BrowseForFiles(
helpPage(), tr("Select %1", "Select whatever type of files the page contains. Example: 'Loader Mods'").arg(displayName()),
m_fileSelectionFilter.arg(displayName()), APPLICATION->settings()->get("CentralModsDir").toString(), this->parentWidget());
@@ -155,9 +207,6 @@ void ExternalResourcesPage::addItem()
void ExternalResourcesPage::removeItem()
{
- if (!m_controlsEnabled)
- return;
-
auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection());
int count = 0;
@@ -201,23 +250,37 @@ void ExternalResourcesPage::removeItem()
void ExternalResourcesPage::removeItems(const QItemSelection& selection)
{
+ if (m_instance != nullptr && m_instance->isRunning()) {
+ auto response = CustomMessageBox::selectable(this, "Confirm Delete",
+ "If you remove this resource while the game is running it may crash your game.\n"
+ "Are you sure you want to do this?",
+ QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No)
+ ->exec();
+
+ if (response != QMessageBox::Yes)
+ return;
+ }
m_model->deleteResources(selection.indexes());
}
void ExternalResourcesPage::enableItem()
{
- if (!m_controlsEnabled)
- return;
-
auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection());
m_model->setResourceEnabled(selection.indexes(), EnableAction::ENABLE);
}
void ExternalResourcesPage::disableItem()
{
- if (!m_controlsEnabled)
- return;
+ if (m_instance != nullptr && m_instance->isRunning()) {
+ auto response = CustomMessageBox::selectable(this, "Confirm disable",
+ "If you disable this resource while the game is running it may crash your game.\n"
+ "Are you sure you want to do this?",
+ QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No)
+ ->exec();
+ if (response != QMessageBox::Yes)
+ return;
+ }
auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection());
m_model->setResourceEnabled(selection.indexes(), EnableAction::DISABLE);
}
@@ -248,6 +311,15 @@ bool ExternalResourcesPage::onSelectionChanged(const QModelIndex& current, const
int row = sourceCurrent.row();
Resource const& resource = m_model->at(row);
ui->frame->updateWithResource(resource);
-
return true;
}
+
+QString ExternalResourcesPage::extraHeaderInfoString()
+{
+ if (ui && ui->treeView && ui->treeView->selectionModel()) {
+ auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes();
+ if (auto count = std::count_if(selection.cbegin(), selection.cend(), [](auto v) { return v.column() == 0; }); count != 0)
+ return tr(" (%1 installed, %2 selected)").arg(m_model->size()).arg(count);
+ }
+ return tr(" (%1 installed)").arg(m_model->size());
+}
diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.h b/launcher/ui/pages/instance/ExternalResourcesPage.h
index d17fbb7f..97d922d8 100644
--- a/launcher/ui/pages/instance/ExternalResourcesPage.h
+++ b/launcher/ui/pages/instance/ExternalResourcesPage.h
@@ -29,6 +29,7 @@ class ExternalResourcesPage : public QMainWindow, public BasePage {
virtual QString helpPage() const override = 0;
virtual bool shouldDisplay() const override = 0;
+ QString extraHeaderInfoString();
void openedImpl() override;
void closedImpl() override;
@@ -60,6 +61,7 @@ class ExternalResourcesPage : public QMainWindow, public BasePage {
virtual void viewConfigs();
void ShowContextMenu(const QPoint& pos);
+ void ShowHeaderContextMenu(const QPoint& pos);
protected:
BaseInstance* m_instance = nullptr;
@@ -71,7 +73,5 @@ class ExternalResourcesPage : public QMainWindow, public BasePage {
QString m_fileSelectionFilter;
QString m_viewFilter;
- bool m_controlsEnabled = true;
-
std::shared_ptr<Setting> m_wide_bar_setting = nullptr;
};
diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.ui b/launcher/ui/pages/instance/ExternalResourcesPage.ui
index 33a03336..3c836691 100644
--- a/launcher/ui/pages/instance/ExternalResourcesPage.ui
+++ b/launcher/ui/pages/instance/ExternalResourcesPage.ui
@@ -62,6 +62,9 @@
<property name="dragDropMode">
<enum>QAbstractItemView::DropOnly</enum>
</property>
+ <property name="uniformRowHeights">
+ <bool>true</bool>
+ </property>
</widget>
</item>
</layout>
@@ -154,6 +157,17 @@
<string>Try to check or update all selected resources (all resources if none are selected)</string>
</property>
</action>
+ <action name="actionVisitItemPage">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="text">
+ <string>Visit mod's page</string>
+ </property>
+ <property name="toolTip">
+ <string>Go to mods home page</string>
+ </property>
+ </action>
</widget>
<customwidgets>
<customwidget>
diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.cpp b/launcher/ui/pages/instance/InstanceSettingsPage.cpp
index 4b4c73dc..943ff17f 100644
--- a/launcher/ui/pages/instance/InstanceSettingsPage.cpp
+++ b/launcher/ui/pages/instance/InstanceSettingsPage.cpp
@@ -60,21 +60,14 @@ InstanceSettingsPage::InstanceSettingsPage(BaseInstance *inst, QWidget *parent)
m_settings = inst->settings();
ui->setupUi(this);
- 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->instanceAccountSelector->setMenu(accountMenu);
-
connect(ui->openGlobalJavaSettingsButton, &QCommandLinkButton::clicked, this, &InstanceSettingsPage::globalSettingsButtonClicked);
connect(APPLICATION, &Application::globalSettingsAboutToOpen, this, &InstanceSettingsPage::applySettings);
connect(APPLICATION, &Application::globalSettingsClosed, this, &InstanceSettingsPage::loadSettings);
+ connect(ui->instanceAccountSelector, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
+ &InstanceSettingsPage::changeInstanceAccount);
loadSettings();
- updateThresholds();
-}
-bool InstanceSettingsPage::shouldDisplay() const
-{
- return !m_instance->isRunning();
+ updateThresholds();
}
InstanceSettingsPage::~InstanceSettingsPage()
@@ -88,12 +81,12 @@ void InstanceSettingsPage::globalSettingsButtonClicked(bool)
case 0:
APPLICATION->ShowGlobalSettings(this, "java-settings");
return;
- case 1:
- APPLICATION->ShowGlobalSettings(this, "minecraft-settings");
- return;
case 2:
APPLICATION->ShowGlobalSettings(this, "custom-commands");
return;
+ default:
+ APPLICATION->ShowGlobalSettings(this, "minecraft-settings");
+ return;
}
}
@@ -454,36 +447,17 @@ void InstanceSettingsPage::on_javaTestBtn_clicked()
void InstanceSettingsPage::updateAccountsMenu()
{
- accountMenu->clear();
-
+ ui->instanceAccountSelector->clear();
auto accounts = APPLICATION->accounts();
int accountIndex = accounts->findAccountByProfileId(m_settings->get("InstanceAccountId").toString());
- MinecraftAccountPtr defaultAccount = accounts->defaultAccount();
-
- if (accountIndex != -1 && accounts->at(accountIndex)) {
- defaultAccount = accounts->at(accountIndex);
- }
-
- if (defaultAccount) {
- ui->instanceAccountSelector->setText(defaultAccount->profileName());
- ui->instanceAccountSelector->setIcon(getFaceForAccount(defaultAccount));
- } else {
- ui->instanceAccountSelector->setText(tr("No default account"));
- ui->instanceAccountSelector->setIcon(APPLICATION->getThemedIcon("noaccount"));
- }
for (int i = 0; i < accounts->count(); i++) {
MinecraftAccountPtr account = accounts->at(i);
- QAction* action = new QAction(account->profileName(), this);
- action->setData(i);
- action->setCheckable(true);
- if (accountIndex == i) {
- action->setChecked(true);
- }
- action->setIcon(getFaceForAccount(account));
- accountMenu->addAction(action);
- connect(action, SIGNAL(triggered(bool)), this, SLOT(changeInstanceAccount()));
+ ui->instanceAccountSelector->addItem(getFaceForAccount(account), account->profileName(), i);
+ if (i == accountIndex)
+ ui->instanceAccountSelector->setCurrentIndex(i);
}
+
}
QIcon InstanceSettingsPage::getFaceForAccount(MinecraftAccountPtr account)
@@ -495,20 +469,13 @@ QIcon InstanceSettingsPage::getFaceForAccount(MinecraftAccountPtr account)
return APPLICATION->getThemedIcon("noaccount");
}
-void InstanceSettingsPage::changeInstanceAccount()
+void InstanceSettingsPage::changeInstanceAccount(int index)
{
- QAction* sAction = (QAction*)sender();
-
- Q_ASSERT(sAction->data().type() == QVariant::Type::Int);
-
- QVariant data = sAction->data();
- int index = data.toInt();
auto accounts = APPLICATION->accounts();
- auto account = accounts->at(index);
- m_settings->set("InstanceAccountId", account->profileId());
-
- ui->instanceAccountSelector->setText(account->profileName());
- ui->instanceAccountSelector->setIcon(getFaceForAccount(account));
+ if (index != -1 && accounts->at(index) && ui->instanceAccountGroupBox->isChecked()) {
+ auto account = accounts->at(index);
+ m_settings->set("InstanceAccountId", account->profileId());
+ }
}
void InstanceSettingsPage::on_maxMemSpinBox_valueChanged(int i)
diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.h b/launcher/ui/pages/instance/InstanceSettingsPage.h
index cb6fbae0..036b4181 100644
--- a/launcher/ui/pages/instance/InstanceSettingsPage.h
+++ b/launcher/ui/pages/instance/InstanceSettingsPage.h
@@ -75,12 +75,11 @@ public:
{
return "Instance-settings";
}
- virtual bool shouldDisplay() const override;
void retranslate() override;
void updateThresholds();
-private slots:
+ private slots:
void on_javaDetectBtn_clicked();
void on_javaTestBtn_clicked();
void on_javaBrowseBtn_clicked();
@@ -95,12 +94,11 @@ private slots:
void updateAccountsMenu();
QIcon getFaceForAccount(MinecraftAccountPtr account);
- void changeInstanceAccount();
+ void changeInstanceAccount(int index);
private:
Ui::InstanceSettingsPage *ui;
BaseInstance *m_instance;
SettingsObjectPtr m_settings;
unique_qobject_ptr<JavaCommon::TestCheck> checker;
- QMenu *accountMenu = nullptr;
};
diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.ui b/launcher/ui/pages/instance/InstanceSettingsPage.ui
index 1b986184..8427965d 100644
--- a/launcher/ui/pages/instance/InstanceSettingsPage.ui
+++ b/launcher/ui/pages/instance/InstanceSettingsPage.ui
@@ -269,7 +269,7 @@
<item>
<widget class="QCheckBox" name="maximizedCheckBox">
<property name="text">
- <string>Start Minecraft maximized?</string>
+ <string>Start Minecraft maximized</string>
</property>
</widget>
</item>
@@ -341,21 +341,21 @@
<item>
<widget class="QCheckBox" name="showConsoleCheck">
<property name="text">
- <string>Show console while the game is running?</string>
+ <string>Show console while the game is running</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="autoCloseConsoleCheck">
<property name="text">
- <string>Automatically close console when the game quits?</string>
+ <string>Automatically close console when the game quits</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="showConsoleErrorCheck">
<property name="text">
- <string>Show console when the game crashes?</string>
+ <string>Show console when the game crashes</string>
</property>
</widget>
</item>
@@ -636,14 +636,7 @@
</widget>
</item>
<item row="0" column="1">
- <widget class="QToolButton" name="instanceAccountSelector">
- <property name="popupMode">
- <enum>QToolButton::InstantPopup</enum>
- </property>
- <property name="toolButtonStyle">
- <enum>Qt::ToolButtonTextBesideIcon</enum>
- </property>
- </widget>
+ <widget class="QComboBox" name="instanceAccountSelector"/>
</item>
</layout>
</item>
diff --git a/launcher/ui/pages/instance/ManagedPackPage.cpp b/launcher/ui/pages/instance/ManagedPackPage.cpp
index dc983d9a..0fc0c986 100644
--- a/launcher/ui/pages/instance/ManagedPackPage.cpp
+++ b/launcher/ui/pages/instance/ManagedPackPage.cpp
@@ -30,8 +30,6 @@ class NoBigComboBoxStyle : public QProxyStyle {
Q_OBJECT
public:
- NoBigComboBoxStyle(QStyle* style) : QProxyStyle(style) {}
-
// clang-format off
int styleHint(QStyle::StyleHint hint, const QStyleOption* option = nullptr, const QWidget* widget = nullptr, QStyleHintReturn* returnData = nullptr) const override
{
@@ -41,6 +39,36 @@ class NoBigComboBoxStyle : public QProxyStyle {
return QProxyStyle::styleHint(hint, option, widget, returnData);
}
// clang-format on
+
+ /**
+ * Something about QProxyStyle and QStyle objects means they can't be free'd just
+ * because all the widgets using them are gone.
+ * They seems to be tied to the QApplicaiton lifecycle.
+ * So make singletons tied to the lifetime of the application to clean them up and ensure they aren't
+ * being remade over and over again, thus leaking memory.
+ */
+ public:
+ static NoBigComboBoxStyle* getInstance(QStyle* style)
+ {
+ static QHash<QStyle*, NoBigComboBoxStyle*> s_singleton_instances_ = {};
+ static std::mutex s_singleton_instances_mutex_;
+
+ std::lock_guard<std::mutex> lock(s_singleton_instances_mutex_);
+ auto inst_iter = s_singleton_instances_.constFind(style);
+ NoBigComboBoxStyle* inst = nullptr;
+ if (inst_iter == s_singleton_instances_.constEnd() || *inst_iter == nullptr) {
+ inst = new NoBigComboBoxStyle(style);
+ inst->setParent(APPLICATION);
+ s_singleton_instances_.insert(style, inst);
+ qDebug() << "QProxyStyle NoBigComboBox created for" << style->objectName() << style;
+ } else {
+ inst = *inst_iter;
+ }
+ return inst;
+ }
+
+ private:
+ NoBigComboBoxStyle(QStyle* style) : QProxyStyle(style) {}
};
ManagedPackPage* ManagedPackPage::createPage(BaseInstance* inst, QString type, QWidget* parent)
@@ -62,11 +90,13 @@ ManagedPackPage::ManagedPackPage(BaseInstance* inst, InstanceWindow* instance_wi
// NOTE: GTK2 themes crash with the proxy style.
// This seems like an upstream bug, so there's not much else that can be done.
- if (!QStyleFactory::keys().contains("gtk2"))
- ui->versionsComboBox->setStyle(new NoBigComboBoxStyle(ui->versionsComboBox->style()));
+ if (!QStyleFactory::keys().contains("gtk2")) {
+ auto comboStyle = NoBigComboBoxStyle::getInstance(ui->versionsComboBox->style());
+ ui->versionsComboBox->setStyle(comboStyle);
+ }
ui->reloadButton->setVisible(false);
- connect(ui->reloadButton, &QPushButton::clicked, this, [this](bool){
+ connect(ui->reloadButton, &QPushButton::clicked, this, [this](bool) {
ui->reloadButton->setVisible(false);
m_loaded = false;
@@ -174,7 +204,7 @@ ModrinthManagedPackPage::ModrinthManagedPackPage(BaseInstance* inst, InstanceWin
{
Q_ASSERT(inst->isManagedPack());
connect(ui->versionsComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(suggestVersion()));
- connect(ui->updateButton, &QPushButton::pressed, this, &ModrinthManagedPackPage::update);
+ connect(ui->updateButton, &QPushButton::clicked, this, &ModrinthManagedPackPage::update);
}
// MODRINTH
@@ -195,7 +225,8 @@ void ModrinthManagedPackPage::parseManagedPack()
QString id = m_inst->getManagedPackID();
- m_fetch_job->addNetAction(Net::Download::makeByteArray(QString("%1/project/%2/version").arg(BuildConfig.MODRINTH_PROD_URL, id), response.get()));
+ m_fetch_job->addNetAction(
+ Net::Download::makeByteArray(QString("%1/project/%2/version").arg(BuildConfig.MODRINTH_PROD_URL, id), response));
QObject::connect(m_fetch_job.get(), &NetJob::succeeded, this, [this, response, id] {
QJsonParseError parse_error{};
@@ -236,7 +267,6 @@ void ModrinthManagedPackPage::parseManagedPack()
if (version.version == m_inst->getManagedPackVersionName())
name = tr("%1 (Current)").arg(name);
-
ui->versionsComboBox->addItem(name, QVariant(version.id));
}
@@ -260,6 +290,10 @@ QString ModrinthManagedPackPage::url() const
void ModrinthManagedPackPage::suggestVersion()
{
auto index = ui->versionsComboBox->currentIndex();
+ if (m_pack.versions.length() == 0) {
+ setFailState();
+ return;
+ }
auto version = m_pack.versions.at(index);
ui->changelogTextBrowser->setHtml(markdownToHTML(version.changelog.toUtf8()));
@@ -270,6 +304,10 @@ void ModrinthManagedPackPage::suggestVersion()
void ModrinthManagedPackPage::update()
{
auto index = ui->versionsComboBox->currentIndex();
+ if (m_pack.versions.length() == 0) {
+ setFailState();
+ return;
+ }
auto version = m_pack.versions.at(index);
QMap<QString, QString> extra_info;
@@ -301,7 +339,7 @@ FlameManagedPackPage::FlameManagedPackPage(BaseInstance* inst, InstanceWindow* i
{
Q_ASSERT(inst->isManagedPack());
connect(ui->versionsComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(suggestVersion()));
- connect(ui->updateButton, &QPushButton::pressed, this, &FlameManagedPackPage::update);
+ connect(ui->updateButton, &QPushButton::clicked, this, &FlameManagedPackPage::update);
}
void FlameManagedPackPage::parseManagedPack()
@@ -338,7 +376,7 @@ void FlameManagedPackPage::parseManagedPack()
QString id = m_inst->getManagedPackID();
- m_fetch_job->addNetAction(Net::Download::makeByteArray(QString("%1/mods/%2/files").arg(BuildConfig.FLAME_BASE_URL, id), response.get()));
+ m_fetch_job->addNetAction(Net::Download::makeByteArray(QString("%1/mods/%2/files").arg(BuildConfig.FLAME_BASE_URL, id), response));
QObject::connect(m_fetch_job.get(), &NetJob::succeeded, this, [this, response, id] {
QJsonParseError parse_error{};
@@ -398,6 +436,10 @@ QString FlameManagedPackPage::url() const
void FlameManagedPackPage::suggestVersion()
{
auto index = ui->versionsComboBox->currentIndex();
+ if (m_pack.versions.length() == 0) {
+ setFailState();
+ return;
+ }
auto version = m_pack.versions.at(index);
ui->changelogTextBrowser->setHtml(m_api.getModFileChangelog(m_inst->getManagedPackID().toInt(), version.fileId));
@@ -408,6 +450,10 @@ void FlameManagedPackPage::suggestVersion()
void FlameManagedPackPage::update()
{
auto index = ui->versionsComboBox->currentIndex();
+ if (m_pack.versions.length() == 0) {
+ setFailState();
+ return;
+ }
auto version = m_pack.versions.at(index);
QMap<QString, QString> extra_info;
diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp
index 4548af59..cef292bd 100644
--- a/launcher/ui/pages/instance/ModFolderPage.cpp
+++ b/launcher/ui/pages/instance/ModFolderPage.cpp
@@ -4,6 +4,7 @@
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
+ * Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -44,6 +45,7 @@
#include <QMenu>
#include <QMessageBox>
#include <QSortFilterProxyModel>
+#include <algorithm>
#include "Application.h"
@@ -59,6 +61,7 @@
#include "minecraft/mod/Mod.h"
#include "minecraft/mod/ModFolderModel.h"
+#include "modplatform/ModIndex.h"
#include "modplatform/ResourceAPI.h"
#include "Version.h"
@@ -85,45 +88,40 @@ ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr<ModFolderModel>
ui->actionsToolbar->insertActionAfter(ui->actionAddItem, ui->actionUpdateItem);
connect(ui->actionUpdateItem, &QAction::triggered, this, &ModFolderPage::updateMods);
- auto check_allow_update = [this] {
- return (!m_instance || !m_instance->isRunning()) &&
- (ui->treeView->selectionModel()->hasSelection() || !m_model->empty());
- };
+ ui->actionVisitItemPage->setToolTip(tr("Go to mod's home page"));
+ ui->actionsToolbar->addAction(ui->actionVisitItemPage);
+ connect(ui->actionVisitItemPage, &QAction::triggered, this, &ModFolderPage::visitModPages);
- connect(ui->treeView->selectionModel(), &QItemSelectionModel::selectionChanged, this, [this, check_allow_update] {
- ui->actionUpdateItem->setEnabled(check_allow_update());
- });
+ auto check_allow_update = [this] { return ui->treeView->selectionModel()->hasSelection() || !m_model->empty(); };
- connect(mods.get(), &ModFolderModel::rowsInserted, this, [this, check_allow_update] {
+ connect(ui->treeView->selectionModel(), &QItemSelectionModel::selectionChanged, this, [this, check_allow_update] {
ui->actionUpdateItem->setEnabled(check_allow_update());
- });
- connect(mods.get(), &ModFolderModel::rowsRemoved, this, [this, check_allow_update] {
- ui->actionUpdateItem->setEnabled(check_allow_update());
+ auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes();
+ auto mods_list = m_model->selectedMods(selection);
+ auto selected = std::count_if(mods_list.cbegin(), mods_list.cend(),
+ [](Mod* v) { return v->metadata() != nullptr || v->homeurl().size() != 0; });
+ if (selected <= 1) {
+ ui->actionVisitItemPage->setText(tr("Visit mod's page"));
+ ui->actionVisitItemPage->setToolTip(tr("Go to mod's home page"));
+ } else {
+ ui->actionVisitItemPage->setText(tr("Visit mods' pages"));
+ ui->actionVisitItemPage->setToolTip(tr("Go to the pages of the selected mods"));
+ }
+ ui->actionVisitItemPage->setEnabled(selected != 0);
});
- connect(mods.get(), &ModFolderModel::updateFinished, this, [this, check_allow_update, mods] {
- ui->actionUpdateItem->setEnabled(check_allow_update());
+ connect(mods.get(), &ModFolderModel::rowsInserted, this,
+ [this, check_allow_update] { ui->actionUpdateItem->setEnabled(check_allow_update()); });
- // Prevent a weird crash when trying to open the mods page twice in a session o.O
- disconnect(mods.get(), &ModFolderModel::updateFinished, this, 0);
- });
+ connect(mods.get(), &ModFolderModel::rowsRemoved, this,
+ [this, check_allow_update] { ui->actionUpdateItem->setEnabled(check_allow_update()); });
- connect(m_instance, &BaseInstance::runningStatusChanged, this, &ModFolderPage::runningStateChanged);
- ModFolderPage::runningStateChanged(m_instance && m_instance->isRunning());
+ connect(mods.get(), &ModFolderModel::updateFinished, this,
+ [this, check_allow_update] { ui->actionUpdateItem->setEnabled(check_allow_update()); });
}
}
-void ModFolderPage::runningStateChanged(bool running)
-{
- ui->actionDownloadItem->setEnabled(!running);
- ui->actionUpdateItem->setEnabled(!running);
- ui->actionAddItem->setEnabled(!running);
- ui->actionEnableItem->setEnabled(!running);
- ui->actionDisableItem->setEnabled(!running);
- ui->actionRemoveItem->setEnabled(!running);
-}
-
bool ModFolderPage::shouldDisplay() const
{
return true;
@@ -140,15 +138,23 @@ bool ModFolderPage::onSelectionChanged(const QModelIndex& current, const QModelI
return true;
}
-void ModFolderPage::removeItems(const QItemSelection &selection)
+void ModFolderPage::removeItems(const QItemSelection& selection)
{
+ if (m_instance != nullptr && m_instance->isRunning()) {
+ auto response = CustomMessageBox::selectable(this, "Confirm Delete",
+ "If you remove mods while the game is running it may crash your game.\n"
+ "Are you sure you want to do this?",
+ QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No)
+ ->exec();
+
+ if (response != QMessageBox::Yes)
+ return;
+ }
m_model->deleteMods(selection.indexes());
}
void ModFolderPage::installMods()
{
- if (!m_controlsEnabled)
- return;
if (m_instance->typeName() != "Minecraft")
return; // this is a null instance or a legacy instance
@@ -214,8 +220,7 @@ void ModFolderPage::updateMods()
message = tr("All selected mods are up-to-date! :)");
}
}
- CustomMessageBox::selectable(this, tr("Update checker"), message)
- ->exec();
+ CustomMessageBox::selectable(this, tr("Update checker"), message)->exec();
return;
}
@@ -282,3 +287,13 @@ bool NilModFolderPage::shouldDisplay() const
{
return m_model->dir().exists();
}
+
+void ModFolderPage::visitModPages()
+{
+ auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes();
+ for (auto mod : m_model->selectedMods(selection)) {
+ auto url = mod->metaurl();
+ if (!url.isEmpty())
+ DesktopServices::openUrl(url);
+ }
+}
diff --git a/launcher/ui/pages/instance/ModFolderPage.h b/launcher/ui/pages/instance/ModFolderPage.h
index 2fc7b574..a23dcae1 100644
--- a/launcher/ui/pages/instance/ModFolderPage.h
+++ b/launcher/ui/pages/instance/ModFolderPage.h
@@ -4,6 +4,7 @@
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
+ * Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -59,11 +60,11 @@ class ModFolderPage : public ExternalResourcesPage {
bool onSelectionChanged(const QModelIndex& current, const QModelIndex& previous) override;
private slots:
- void runningStateChanged(bool running);
- void removeItems(const QItemSelection &selection) override;
+ void removeItems(const QItemSelection& selection) override;
void installMods();
void updateMods();
+ void visitModPages();
protected:
std::shared_ptr<ModFolderModel> m_model;
diff --git a/launcher/ui/pages/instance/ResourcePackPage.cpp b/launcher/ui/pages/instance/ResourcePackPage.cpp
index 24bfb38d..12b371df 100644
--- a/launcher/ui/pages/instance/ResourcePackPage.cpp
+++ b/launcher/ui/pages/instance/ResourcePackPage.cpp
@@ -67,8 +67,6 @@ bool ResourcePackPage::onSelectionChanged(const QModelIndex& current, const QMod
void ResourcePackPage::downloadRPs()
{
- if (!m_controlsEnabled)
- return;
if (m_instance->typeName() != "Minecraft")
return; // this is a null instance or a legacy instance
diff --git a/launcher/ui/pages/instance/ScreenshotsPage.cpp b/launcher/ui/pages/instance/ScreenshotsPage.cpp
index ca368d3b..bcce5f57 100644
--- a/launcher/ui/pages/instance/ScreenshotsPage.cpp
+++ b/launcher/ui/pages/instance/ScreenshotsPage.cpp
@@ -36,6 +36,7 @@
*/
#include "ScreenshotsPage.h"
+#include "BuildConfig.h"
#include "ui_ScreenshotsPage.h"
#include <QModelIndex>
@@ -96,37 +97,30 @@ public:
return;
if ((info.suffix().compare("png", Qt::CaseInsensitive) != 0))
return;
- int tries = 5;
- while (tries)
- {
- if (!m_cache->stale(m_path))
- return;
- QImage image(m_path);
- if (image.isNull())
- {
- QThread::msleep(500);
- tries--;
- continue;
- }
- QImage small;
- if (image.width() > image.height())
- small = image.scaledToWidth(512).scaledToWidth(256, Qt::SmoothTransformation);
- else
- small = image.scaledToHeight(512).scaledToHeight(256, Qt::SmoothTransformation);
- QPoint offset((256 - small.width()) / 2, (256 - small.height()) / 2);
- QImage square(QSize(256, 256), QImage::Format_ARGB32);
- square.fill(Qt::transparent);
-
- QPainter painter(&square);
- painter.drawImage(offset, small);
- painter.end();
-
- QIcon icon(QPixmap::fromImage(square));
- m_cache->add(m_path, icon);
- m_resultEmitter.emitResultsReady(m_path);
+ if (!m_cache->stale(m_path))
+ return;
+ QImage image(m_path);
+ if (image.isNull()) {
+ m_resultEmitter.emitResultsFailed(m_path);
+ qDebug() << "Error loading screenshot: " + m_path + ". Perhaps too large?";
return;
}
- m_resultEmitter.emitResultsFailed(m_path);
+ QImage small;
+ if (image.width() > image.height())
+ small = image.scaledToWidth(512).scaledToWidth(256, Qt::SmoothTransformation);
+ else
+ small = image.scaledToHeight(512).scaledToHeight(256, Qt::SmoothTransformation);
+ QPoint offset((256 - small.width()) / 2, (256 - small.height()) / 2);
+ QImage square(QSize(256, 256), QImage::Format_ARGB32);
+ square.fill(Qt::transparent);
+
+ QPainter painter(&square);
+ painter.drawImage(offset, small);
+ painter.end();
+
+ QIcon icon(QPixmap::fromImage(square));
+ m_cache->add(m_path, icon);
+ m_resultEmitter.emitResultsReady(m_path);
}
QString m_path;
SharedIconCachePtr m_cache;
@@ -145,9 +139,12 @@ public:
m_thumbnailCache = std::make_shared<SharedIconCache>();
m_thumbnailCache->add("placeholder", APPLICATION->getThemedIcon("screenshot-placeholder"));
connect(&watcher, SIGNAL(fileChanged(QString)), SLOT(fileChanged(QString)));
- // FIXME: the watched file set is not updated when files are removed
}
- virtual ~FilterModel() { m_thumbnailingPool.waitForDone(500); }
+ virtual ~FilterModel() {
+ m_thumbnailingPool.clear();
+ if (!m_thumbnailingPool.waitForDone(500))
+ qDebug() << "Thumbnail pool took longer than 500ms to finish";
+ }
virtual QVariant data(const QModelIndex &proxyIndex, int role = Qt::DisplayRole) const
{
auto model = sourceModel();
@@ -214,10 +211,12 @@ private slots:
void fileChanged(QString filepath)
{
m_thumbnailCache->setStale(filepath);
- thumbnailImage(filepath);
// reinsert the path...
watcher.removePath(filepath);
- watcher.addPath(filepath);
+ if (QFile::exists(filepath)) {
+ watcher.addPath(filepath);
+ thumbnailImage(filepath);
+ }
}
private:
@@ -380,16 +379,18 @@ void ScreenshotsPage::on_actionUpload_triggered()
if (selection.isEmpty())
return;
-
QString text;
+ QUrl baseUrl(BuildConfig.IMGUR_BASE_URL);
if (selection.size() > 1)
- text = tr("You are about to upload %1 screenshots.\n\n"
+ text = tr("You are about to upload %1 screenshots to %2.\n"
+ "You should double-check for personal information.\n\n"
"Are you sure?")
- .arg(selection.size());
+ .arg(QString::number(selection.size()), baseUrl.host());
else
- text =
- tr("You are about to upload the selected screenshot.\n\n"
- "Are you sure?");
+ text = tr("You are about to upload the selected screenshot to %1.\n"
+ "You should double-check for personal information.\n\n"
+ "Are you sure?")
+ .arg(baseUrl.host());
auto response = CustomMessageBox::selectable(this, "Confirm Upload", text, QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No,
QMessageBox::No)
diff --git a/launcher/ui/pages/instance/ShaderPackPage.cpp b/launcher/ui/pages/instance/ShaderPackPage.cpp
index 2d0c10aa..dc8b0a05 100644
--- a/launcher/ui/pages/instance/ShaderPackPage.cpp
+++ b/launcher/ui/pages/instance/ShaderPackPage.cpp
@@ -46,7 +46,6 @@
#include "ui/dialogs/ProgressDialog.h"
#include "ui/dialogs/ResourceDownloadDialog.h"
-
ShaderPackPage::ShaderPackPage(MinecraftInstance* instance, std::shared_ptr<ShaderPackFolderModel> model, QWidget* parent)
: ExternalResourcesPage(instance, model, parent)
{
@@ -61,8 +60,6 @@ ShaderPackPage::ShaderPackPage(MinecraftInstance* instance, std::shared_ptr<Shad
void ShaderPackPage::downloadShaders()
{
- if (!m_controlsEnabled)
- return;
if (m_instance->typeName() != "Minecraft")
return; // this is a null instance or a legacy instance
diff --git a/launcher/ui/pages/instance/TexturePackPage.cpp b/launcher/ui/pages/instance/TexturePackPage.cpp
index 427aba11..e477ceda 100644
--- a/launcher/ui/pages/instance/TexturePackPage.cpp
+++ b/launcher/ui/pages/instance/TexturePackPage.cpp
@@ -69,8 +69,6 @@ bool TexturePackPage::onSelectionChanged(const QModelIndex& current, const QMode
void TexturePackPage::downloadTPs()
{
- if (!m_controlsEnabled)
- return;
if (m_instance->typeName() != "Minecraft")
return; // this is a null instance or a legacy instance
diff --git a/launcher/ui/pages/instance/VersionPage.cpp b/launcher/ui/pages/instance/VersionPage.cpp
index 7fff3b93..a180c804 100644
--- a/launcher/ui/pages/instance/VersionPage.cpp
+++ b/launcher/ui/pages/instance/VersionPage.cpp
@@ -40,14 +40,13 @@
#include "Application.h"
-#include <QMessageBox>
-#include <QLabel>
+#include <QAbstractItemModel>
#include <QEvent>
#include <QKeyEvent>
+#include <QLabel>
+#include <QListView>
#include <QMenu>
-#include <QAbstractItemModel>
#include <QMessageBox>
-#include <QListView>
#include <QString>
#include <QUrl>
@@ -55,49 +54,42 @@
#include "ui_VersionPage.h"
#include "ui/dialogs/CustomMessageBox.h"
-#include "ui/dialogs/VersionSelectDialog.h"
#include "ui/dialogs/NewComponentDialog.h"
#include "ui/dialogs/ProgressDialog.h"
+#include "ui/dialogs/VersionSelectDialog.h"
#include "ui/GuiUtil.h"
+#include "DesktopServices.h"
+#include "Exception.h"
+#include "Version.h"
+#include "icons/IconList.h"
#include "minecraft/PackProfile.h"
#include "minecraft/auth/AccountList.h"
#include "minecraft/mod/Mod.h"
-#include "icons/IconList.h"
-#include "Exception.h"
-#include "Version.h"
-#include "DesktopServices.h"
#include "meta/Index.h"
#include "meta/VersionList.h"
-class IconProxy : public QIdentityProxyModel
-{
+class IconProxy : public QIdentityProxyModel {
Q_OBJECT
-public:
-
- IconProxy(QWidget *parentWidget) : QIdentityProxyModel(parentWidget)
+ public:
+ IconProxy(QWidget* parentWidget) : QIdentityProxyModel(parentWidget)
{
connect(parentWidget, &QObject::destroyed, this, &IconProxy::widgetGone);
m_parentWidget = parentWidget;
}
- virtual QVariant data(const QModelIndex &proxyIndex, int role = Qt::DisplayRole) const override
+ virtual QVariant data(const QModelIndex& proxyIndex, int role = Qt::DisplayRole) const override
{
QVariant var = QIdentityProxyModel::data(proxyIndex, role);
int column = proxyIndex.column();
- if(column == 0 && role == Qt::DecorationRole && m_parentWidget)
- {
- if(!var.isNull())
- {
+ if (column == 0 && role == Qt::DecorationRole && m_parentWidget) {
+ if (!var.isNull()) {
auto string = var.toString();
- if(string == "warning")
- {
+ if (string == "warning") {
return APPLICATION->getThemedIcon("status-yellow");
- }
- else if(string == "error")
- {
+ } else if (string == "error") {
return APPLICATION->getThemedIcon("status-bad");
}
}
@@ -105,14 +97,11 @@ public:
}
return var;
}
-private slots:
- void widgetGone()
- {
- m_parentWidget = nullptr;
- }
+ private slots:
+ void widgetGone() { m_parentWidget = nullptr; }
-private:
- QWidget *m_parentWidget = nullptr;
+ private:
+ QWidget* m_parentWidget = nullptr;
};
QIcon VersionPage::icon() const
@@ -144,15 +133,14 @@ void VersionPage::closedImpl()
m_wide_bar_setting->set(ui->toolBar->getVisibilityState());
}
-QMenu * VersionPage::createPopupMenu()
+QMenu* VersionPage::createPopupMenu()
{
QMenu* filteredMenu = QMainWindow::createPopupMenu();
- filteredMenu->removeAction( ui->toolBar->toggleViewAction() );
+ filteredMenu->removeAction(ui->toolBar->toggleViewAction());
return filteredMenu;
}
-VersionPage::VersionPage(MinecraftInstance *inst, QWidget *parent)
- : QMainWindow(parent), ui(new Ui::VersionPage), m_inst(inst)
+VersionPage::VersionPage(MinecraftInstance* inst, QWidget* parent) : QMainWindow(parent), ui(new Ui::VersionPage), m_inst(inst)
{
ui->setupUi(this);
@@ -165,7 +153,7 @@ VersionPage::VersionPage(MinecraftInstance *inst, QWidget *parent)
auto proxy = new IconProxy(ui->packageView);
proxy->setSourceModel(m_profile.get());
- m_filterModel = new QSortFilterProxyModel();
+ m_filterModel = new QSortFilterProxyModel(this);
m_filterModel->setDynamicSortFilter(true);
m_filterModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
m_filterModel->setSortCaseSensitivity(Qt::CaseInsensitive);
@@ -182,10 +170,8 @@ VersionPage::VersionPage(MinecraftInstance *inst, QWidget *parent)
connect(smodel, &QItemSelectionModel::currentChanged, this, &VersionPage::packageCurrent);
connect(m_profile.get(), &PackProfile::minecraftChanged, this, &VersionPage::updateVersionControls);
- controlsEnabled = !m_inst->isRunning();
updateVersionControls();
preselect(0);
- connect(m_inst, &BaseInstance::runningStatusChanged, this, &VersionPage::updateRunningStatus);
connect(ui->packageView, &ModListView::customContextMenuRequested, this, &VersionPage::showContextMenu);
connect(ui->filterEdit, &QLineEdit::textChanged, this, &VersionPage::onFilterTextChanged);
}
@@ -202,18 +188,16 @@ void VersionPage::showContextMenu(const QPoint& pos)
delete menu;
}
-void VersionPage::packageCurrent(const QModelIndex &current, const QModelIndex &previous)
+void VersionPage::packageCurrent(const QModelIndex& current, const QModelIndex& previous)
{
- if (!current.isValid())
- {
+ if (!current.isValid()) {
ui->frame->clear();
return;
}
int row = current.row();
auto patch = m_profile->getComponent(row);
auto severity = patch->getProblemSeverity();
- switch(severity)
- {
+ switch (severity) {
case ProblemSeverity::Warning:
ui->frame->setName(tr("%1 possibly has issues.").arg(patch->getName()));
break;
@@ -226,16 +210,12 @@ void VersionPage::packageCurrent(const QModelIndex &current, const QModelIndex &
return;
}
- auto &problems = patch->getProblems();
+ auto& problems = patch->getProblems();
QString problemOut;
- for (auto &problem: problems)
- {
- if(problem.m_severity == ProblemSeverity::Error)
- {
+ for (auto& problem : problems) {
+ if (problem.m_severity == ProblemSeverity::Error) {
problemOut += tr("Error: ");
- }
- else if(problem.m_severity == ProblemSeverity::Warning)
- {
+ } else if (problem.m_severity == ProblemSeverity::Warning) {
problemOut += tr("Warning: ");
}
problemOut += problem.m_description;
@@ -244,72 +224,47 @@ void VersionPage::packageCurrent(const QModelIndex &current, const QModelIndex &
ui->frame->setDescription(problemOut);
}
-void VersionPage::updateRunningStatus(bool running)
-{
- if(controlsEnabled == running) {
- controlsEnabled = !running;
- updateVersionControls();
- }
-}
-
void VersionPage::updateVersionControls()
{
// FIXME: this is a dirty hack
auto minecraftVersion = Version(m_profile->getComponentVersion("net.minecraft"));
- ui->actionInstall_Forge->setEnabled(controlsEnabled);
-
bool supportsFabric = minecraftVersion >= Version("1.14");
- ui->actionInstall_Fabric->setEnabled(controlsEnabled && supportsFabric);
+ ui->actionInstall_Fabric->setEnabled(supportsFabric);
bool supportsQuilt = minecraftVersion >= Version("1.14");
- ui->actionInstall_Quilt->setEnabled(controlsEnabled && supportsQuilt);
+ ui->actionInstall_Quilt->setEnabled(supportsQuilt);
bool supportsLiteLoader = minecraftVersion <= Version("1.12.2");
- ui->actionInstall_LiteLoader->setEnabled(controlsEnabled && supportsLiteLoader);
+ ui->actionInstall_LiteLoader->setEnabled(supportsLiteLoader);
updateButtons();
}
void VersionPage::updateButtons(int row)
{
- if(row == -1)
+ if (row == -1)
row = currentRow();
auto patch = m_profile->getComponent(row);
- ui->actionRemove->setEnabled(controlsEnabled && patch && patch->isRemovable());
- ui->actionMove_down->setEnabled(controlsEnabled && patch && patch->isMoveable());
- ui->actionMove_up->setEnabled(controlsEnabled && patch && patch->isMoveable());
- ui->actionChange_version->setEnabled(controlsEnabled && patch && patch->isVersionChangeable());
- ui->actionEdit->setEnabled(controlsEnabled && patch && patch->isCustom());
- ui->actionCustomize->setEnabled(controlsEnabled && patch && patch->isCustomizable());
- ui->actionRevert->setEnabled(controlsEnabled && patch && patch->isRevertible());
- ui->actionDownload_All->setEnabled(controlsEnabled);
- ui->actionAdd_Empty->setEnabled(controlsEnabled);
- ui->actionImport_Components->setEnabled(controlsEnabled);
- ui->actionReload->setEnabled(controlsEnabled);
- ui->actionInstall_mods->setEnabled(controlsEnabled);
- ui->actionReplace_Minecraft_jar->setEnabled(controlsEnabled);
- ui->actionAdd_to_Minecraft_jar->setEnabled(controlsEnabled);
- ui->actionAdd_Agents->setEnabled(controlsEnabled);
+ ui->actionRemove->setEnabled(patch && patch->isRemovable());
+ ui->actionMove_down->setEnabled(patch && patch->isMoveable());
+ ui->actionMove_up->setEnabled(patch && patch->isMoveable());
+ ui->actionChange_version->setEnabled(patch && patch->isVersionChangeable());
+ ui->actionEdit->setEnabled(patch && patch->isCustom());
+ ui->actionCustomize->setEnabled(patch && patch->isCustomizable());
+ ui->actionRevert->setEnabled(patch && patch->isRevertible());
}
bool VersionPage::reloadPackProfile()
{
- try
- {
+ try {
m_profile->reload(Net::Mode::Online);
return true;
- }
- catch (const Exception &e)
- {
+ } catch (const Exception& e) {
QMessageBox::critical(this, tr("Error"), e.cause());
return false;
- }
- catch (...)
- {
- QMessageBox::critical(
- this, tr("Error"),
- tr("Couldn't load the instance profile."));
+ } catch (...) {
+ QMessageBox::critical(this, tr("Error"), tr("Couldn't load the instance profile."));
return false;
}
}
@@ -322,14 +277,12 @@ void VersionPage::on_actionReload_triggered()
void VersionPage::on_actionRemove_triggered()
{
- if (!ui->packageView->currentIndex().isValid())
- {
+ if (!ui->packageView->currentIndex().isValid()) {
return;
}
int index = ui->packageView->currentIndex().row();
auto component = m_profile->getComponent(index);
- if (component->isCustom())
- {
+ if (component->isCustom()) {
auto response = CustomMessageBox::selectable(this, tr("Confirm Removal"),
tr("You are about to remove \"%1\".\n"
"This is permanent and will completely remove the custom component.\n\n"
@@ -342,8 +295,7 @@ void VersionPage::on_actionRemove_triggered()
return;
}
// FIXME: use actual model, not reloading.
- if (!m_profile->remove(index))
- {
+ if (!m_profile->remove(index)) {
QMessageBox::critical(this, tr("Error"), tr("Couldn't remove file"));
}
updateButtons();
@@ -353,17 +305,16 @@ void VersionPage::on_actionRemove_triggered()
void VersionPage::on_actionInstall_mods_triggered()
{
- if(m_container)
- {
+ if (m_container) {
m_container->selectPage("mods");
}
}
void VersionPage::on_actionAdd_to_Minecraft_jar_triggered()
{
- auto list = GuiUtil::BrowseForFiles("jarmod", tr("Select jar mods"), tr("Minecraft.jar mods (*.zip *.jar)"), APPLICATION->settings()->get("CentralModsDir").toString(), this->parentWidget());
- if(!list.empty())
- {
+ auto list = GuiUtil::BrowseForFiles("jarmod", tr("Select jar mods"), tr("Minecraft.jar mods (*.zip *.jar)"),
+ APPLICATION->settings()->get("CentralModsDir").toString(), this->parentWidget());
+ if (!list.empty()) {
m_profile->installJarMods(list);
}
updateButtons();
@@ -371,9 +322,9 @@ void VersionPage::on_actionAdd_to_Minecraft_jar_triggered()
void VersionPage::on_actionReplace_Minecraft_jar_triggered()
{
- auto jarPath = GuiUtil::BrowseForFile("jar", tr("Select jar"), tr("Minecraft.jar replacement (*.jar)"), APPLICATION->settings()->get("CentralModsDir").toString(), this->parentWidget());
- if(!jarPath.isEmpty())
- {
+ auto jarPath = GuiUtil::BrowseForFile("jar", tr("Select jar"), tr("Minecraft.jar replacement (*.jar)"),
+ APPLICATION->settings()->get("CentralModsDir").toString(), this->parentWidget());
+ if (!jarPath.isEmpty()) {
m_profile->installCustomJar(jarPath);
}
updateButtons();
@@ -407,12 +358,9 @@ void VersionPage::on_actionAdd_Agents_triggered()
void VersionPage::on_actionMove_up_triggered()
{
- try
- {
+ try {
m_profile->move(currentRow(), PackProfile::MoveUp);
- }
- catch (const Exception &e)
- {
+ } catch (const Exception& e) {
QMessageBox::critical(this, tr("Error"), e.cause());
}
updateButtons();
@@ -420,12 +368,9 @@ void VersionPage::on_actionMove_up_triggered()
void VersionPage::on_actionMove_down_triggered()
{
- try
- {
+ try {
m_profile->move(currentRow(), PackProfile::MoveDown);
- }
- catch (const Exception &e)
- {
+ } catch (const Exception& e) {
QMessageBox::critical(this, tr("Error"), e.cause());
}
updateButtons();
@@ -434,39 +379,32 @@ void VersionPage::on_actionMove_down_triggered()
void VersionPage::on_actionChange_version_triggered()
{
auto versionRow = currentRow();
- if(versionRow == -1)
- {
+ if (versionRow == -1) {
return;
}
auto patch = m_profile->getComponent(versionRow);
auto name = patch->getName();
auto list = patch->getVersionList();
- if(!list)
- {
+ if (!list) {
return;
}
auto uid = list->uid();
// FIXME: this is a horrible HACK. Get version filtering information from the actual metadata...
- if(uid == "net.minecraftforge")
- {
+ if (uid == "net.minecraftforge") {
on_actionInstall_Forge_triggered();
return;
- }
- else if (uid == "com.mumfrey.liteloader")
- {
+ } else if (uid == "com.mumfrey.liteloader") {
on_actionInstall_LiteLoader_triggered();
return;
}
VersionSelectDialog vselect(list.get(), tr("Change %1 version").arg(name), this);
- if (uid == "net.fabricmc.intermediary" || uid == "org.quiltmc.hashed")
- {
+ if (uid == "net.fabricmc.intermediary" || uid == "org.quiltmc.hashed") {
vselect.setEmptyString(tr("No intermediary mappings versions are currently available."));
vselect.setEmptyErrorString(tr("Couldn't load or download the intermediary mappings version lists!"));
vselect.setExactFilter(BaseVersionList::ParentVersionRole, m_profile->getComponentVersion("net.minecraft"));
}
auto currentVersion = patch->getVersion();
- if(!currentVersion.isEmpty())
- {
+ if (!currentVersion.isEmpty()) {
vselect.setCurrentVersion(currentVersion);
}
if (!vselect.exec() || !vselect.selectedVersion())
@@ -474,8 +412,7 @@ void VersionPage::on_actionChange_version_triggered()
qDebug() << "Change" << uid << "to" << vselect.selectedVersion()->descriptor();
bool important = false;
- if(uid == "net.minecraft")
- {
+ if (uid == "net.minecraft") {
important = true;
}
m_profile->setComponentVersion(uid, vselect.selectedVersion()->descriptor(), important);
@@ -485,23 +422,21 @@ void VersionPage::on_actionChange_version_triggered()
void VersionPage::on_actionDownload_All_triggered()
{
- if (!APPLICATION->accounts()->anyAccountIsValid())
- {
- CustomMessageBox::selectable(
- this, tr("Error"),
- tr("Cannot download Minecraft or update instances unless you have at least "
- "one account added.\nPlease add your Mojang or Minecraft account."),
- QMessageBox::Warning)->show();
+ if (!APPLICATION->accounts()->anyAccountIsValid()) {
+ CustomMessageBox::selectable(this, tr("Error"),
+ tr("Cannot download Minecraft or update instances unless you have at least "
+ "one account added.\nPlease add your Mojang or Minecraft account."),
+ QMessageBox::Warning)
+ ->show();
return;
}
auto updateTask = m_inst->createUpdateTask(Net::Mode::Online);
- if (!updateTask)
- {
+ if (!updateTask) {
return;
}
ProgressDialog tDialog(this);
- connect(updateTask.get(), SIGNAL(failed(QString)), SLOT(onGameUpdateError(QString)));
+ connect(updateTask.get(), &Task::failed, this, &VersionPage::onGameUpdateError);
// FIXME: unused return value
tDialog.execWithTask(updateTask.get());
updateButtons();
@@ -511,28 +446,26 @@ void VersionPage::on_actionDownload_All_triggered()
void VersionPage::on_actionInstall_Forge_triggered()
{
auto vlist = APPLICATION->metadataIndex()->get("net.minecraftforge");
- if(!vlist)
- {
+ if (!vlist) {
return;
}
VersionSelectDialog vselect(vlist.get(), tr("Select Forge version"), this);
vselect.setExactFilter(BaseVersionList::ParentVersionRole, m_profile->getComponentVersion("net.minecraft"));
- vselect.setEmptyString(tr("No Forge versions are currently available for Minecraft ") + m_profile->getComponentVersion("net.minecraft"));
+ vselect.setEmptyString(tr("No Forge versions are currently available for Minecraft ") +
+ m_profile->getComponentVersion("net.minecraft"));
vselect.setEmptyErrorString(tr("Couldn't load or download the Forge version lists!"));
auto currentVersion = m_profile->getComponentVersion("net.minecraftforge");
- if(!currentVersion.isEmpty())
- {
+ if (!currentVersion.isEmpty()) {
vselect.setCurrentVersion(currentVersion);
}
- if (vselect.exec() && vselect.selectedVersion())
- {
+ if (vselect.exec() && vselect.selectedVersion()) {
auto vsn = vselect.selectedVersion();
m_profile->setComponentVersion("net.minecraftforge", vsn->descriptor());
m_profile->resolve(Net::Mode::Online);
// m_profile->installVersion();
- preselect(m_profile->rowCount(QModelIndex())-1);
+ preselect(m_profile->rowCount(QModelIndex()) - 1);
m_container->refreshContainer();
}
}
@@ -540,8 +473,7 @@ void VersionPage::on_actionInstall_Forge_triggered()
void VersionPage::on_actionInstall_Fabric_triggered()
{
auto vlist = APPLICATION->metadataIndex()->get("net.fabricmc.fabric-loader");
- if(!vlist)
- {
+ if (!vlist) {
return;
}
VersionSelectDialog vselect(vlist.get(), tr("Select Fabric Loader version"), this);
@@ -549,17 +481,15 @@ void VersionPage::on_actionInstall_Fabric_triggered()
vselect.setEmptyErrorString(tr("Couldn't load or download the Fabric Loader version lists!"));
auto currentVersion = m_profile->getComponentVersion("net.fabricmc.fabric-loader");
- if(!currentVersion.isEmpty())
- {
+ if (!currentVersion.isEmpty()) {
vselect.setCurrentVersion(currentVersion);
}
- if (vselect.exec() && vselect.selectedVersion())
- {
+ if (vselect.exec() && vselect.selectedVersion()) {
auto vsn = vselect.selectedVersion();
m_profile->setComponentVersion("net.fabricmc.fabric-loader", vsn->descriptor());
m_profile->resolve(Net::Mode::Online);
- preselect(m_profile->rowCount(QModelIndex())-1);
+ preselect(m_profile->rowCount(QModelIndex()) - 1);
m_container->refreshContainer();
}
}
@@ -567,8 +497,7 @@ void VersionPage::on_actionInstall_Fabric_triggered()
void VersionPage::on_actionInstall_Quilt_triggered()
{
auto vlist = APPLICATION->metadataIndex()->get("org.quiltmc.quilt-loader");
- if(!vlist)
- {
+ if (!vlist) {
return;
}
VersionSelectDialog vselect(vlist.get(), tr("Select Quilt Loader version"), this);
@@ -576,17 +505,15 @@ void VersionPage::on_actionInstall_Quilt_triggered()
vselect.setEmptyErrorString(tr("Couldn't load or download the Quilt Loader version lists!"));
auto currentVersion = m_profile->getComponentVersion("org.quiltmc.quilt-loader");
- if(!currentVersion.isEmpty())
- {
+ if (!currentVersion.isEmpty()) {
vselect.setCurrentVersion(currentVersion);
}
- if (vselect.exec() && vselect.selectedVersion())
- {
+ if (vselect.exec() && vselect.selectedVersion()) {
auto vsn = vselect.selectedVersion();
m_profile->setComponentVersion("org.quiltmc.quilt-loader", vsn->descriptor());
m_profile->resolve(Net::Mode::Online);
- preselect(m_profile->rowCount(QModelIndex())-1);
+ preselect(m_profile->rowCount(QModelIndex()) - 1);
m_container->refreshContainer();
}
}
@@ -595,14 +522,12 @@ void VersionPage::on_actionAdd_Empty_triggered()
{
NewComponentDialog compdialog(QString(), QString(), this);
QStringList blacklist;
- for(int i = 0; i < m_profile->rowCount(); i++)
- {
+ for (int i = 0; i < m_profile->rowCount(); i++) {
auto comp = m_profile->getComponent(i);
blacklist.push_back(comp->getID());
}
compdialog.setBlacklist(blacklist);
- if (compdialog.exec())
- {
+ if (compdialog.exec()) {
qDebug() << "name:" << compdialog.name();
qDebug() << "uid:" << compdialog.uid();
m_profile->installEmpty(compdialog.uid(), compdialog.name());
@@ -612,28 +537,26 @@ void VersionPage::on_actionAdd_Empty_triggered()
void VersionPage::on_actionInstall_LiteLoader_triggered()
{
auto vlist = APPLICATION->metadataIndex()->get("com.mumfrey.liteloader");
- if(!vlist)
- {
+ if (!vlist) {
return;
}
VersionSelectDialog vselect(vlist.get(), tr("Select LiteLoader version"), this);
vselect.setExactFilter(BaseVersionList::ParentVersionRole, m_profile->getComponentVersion("net.minecraft"));
- vselect.setEmptyString(tr("No LiteLoader versions are currently available for Minecraft ") + m_profile->getComponentVersion("net.minecraft"));
+ vselect.setEmptyString(tr("No LiteLoader versions are currently available for Minecraft ") +
+ m_profile->getComponentVersion("net.minecraft"));
vselect.setEmptyErrorString(tr("Couldn't load or download the LiteLoader version lists!"));
auto currentVersion = m_profile->getComponentVersion("com.mumfrey.liteloader");
- if(!currentVersion.isEmpty())
- {
+ if (!currentVersion.isEmpty()) {
vselect.setCurrentVersion(currentVersion);
}
- if (vselect.exec() && vselect.selectedVersion())
- {
+ if (vselect.exec() && vselect.selectedVersion()) {
auto vsn = vselect.selectedVersion();
m_profile->setComponentVersion("com.mumfrey.liteloader", vsn->descriptor());
m_profile->resolve(Net::Mode::Online);
// m_profile->installVersion(vselect.selectedVersion());
- preselect(m_profile->rowCount(QModelIndex())-1);
+ preselect(m_profile->rowCount(QModelIndex()) - 1);
m_container->refreshContainer();
}
}
@@ -648,7 +571,7 @@ void VersionPage::on_actionMinecraftFolder_triggered()
DesktopServices::openDirectory(m_inst->gameRoot(), true);
}
-void VersionPage::versionCurrent(const QModelIndex &current, const QModelIndex &previous)
+void VersionPage::versionCurrent(const QModelIndex& current, const QModelIndex& previous)
{
currentIdx = current.row();
updateButtons(currentIdx);
@@ -656,16 +579,13 @@ void VersionPage::versionCurrent(const QModelIndex &current, const QModelIndex &
void VersionPage::preselect(int row)
{
- if(row < 0)
- {
+ if (row < 0) {
row = 0;
}
- if(row >= m_profile->rowCount(QModelIndex()))
- {
+ if (row >= m_profile->rowCount(QModelIndex())) {
row = m_profile->rowCount(QModelIndex()) - 1;
}
- if(row < 0)
- {
+ if (row < 0) {
return;
}
auto model_index = m_profile->index(row);
@@ -681,8 +601,7 @@ void VersionPage::onGameUpdateError(QString error)
ComponentPtr VersionPage::current()
{
auto row = currentRow();
- if(row < 0)
- {
+ if (row < 0) {
return nullptr;
}
return m_profile->getComponent(row);
@@ -690,8 +609,7 @@ ComponentPtr VersionPage::current()
int VersionPage::currentRow()
{
- if (ui->packageView->selectionModel()->selectedRows().isEmpty())
- {
+ if (ui->packageView->selectionModel()->selectedRows().isEmpty()) {
return -1;
}
return ui->packageView->selectionModel()->selectedRows().first().row();
@@ -700,18 +618,15 @@ int VersionPage::currentRow()
void VersionPage::on_actionCustomize_triggered()
{
auto version = currentRow();
- if(version == -1)
- {
+ if (version == -1) {
return;
}
auto patch = m_profile->getComponent(version);
- if(!patch->getVersionFile())
- {
+ if (!patch->getVersionFile()) {
// TODO: wait for the update task to finish here...
return;
}
- if(!m_profile->customize(version))
- {
+ if (!m_profile->customize(version)) {
// TODO: some error box here
}
updateButtons();
@@ -721,13 +636,11 @@ void VersionPage::on_actionCustomize_triggered()
void VersionPage::on_actionEdit_triggered()
{
auto version = current();
- if(!version)
- {
+ if (!version) {
return;
}
auto filename = version->getFilename();
- if(!QFileInfo::exists(filename))
- {
+ if (!QFileInfo::exists(filename)) {
qWarning() << "file" << filename << "can't be opened for editing, doesn't exist!";
return;
}
@@ -737,8 +650,7 @@ void VersionPage::on_actionEdit_triggered()
void VersionPage::on_actionRevert_triggered()
{
auto version = currentRow();
- if(version == -1)
- {
+ if (version == -1) {
return;
}
auto component = m_profile->getComponent(version);
@@ -754,8 +666,7 @@ void VersionPage::on_actionRevert_triggered()
if (response != QMessageBox::Yes)
return;
- if(!m_profile->revertToBase(version))
- {
+ if (!m_profile->revertToBase(version)) {
// TODO: some error box here
}
updateButtons();
@@ -763,7 +674,7 @@ void VersionPage::on_actionRevert_triggered()
m_container->refreshContainer();
}
-void VersionPage::onFilterTextChanged(const QString &newContents)
+void VersionPage::onFilterTextChanged(const QString& newContents)
{
m_filterModel->setFilterFixedString(newContents);
}
diff --git a/launcher/ui/pages/instance/VersionPage.h b/launcher/ui/pages/instance/VersionPage.h
index d0087714..45d383f4 100644
--- a/launcher/ui/pages/instance/VersionPage.h
+++ b/launcher/ui/pages/instance/VersionPage.h
@@ -46,38 +46,27 @@
#include "minecraft/PackProfile.h"
#include "ui/pages/BasePage.h"
-namespace Ui
-{
+namespace Ui {
class VersionPage;
}
-class VersionPage : public QMainWindow, public BasePage
-{
+class VersionPage : public QMainWindow, public BasePage {
Q_OBJECT
-public:
- explicit VersionPage(MinecraftInstance *inst, QWidget *parent = 0);
+ public:
+ explicit VersionPage(MinecraftInstance* inst, QWidget* parent = 0);
virtual ~VersionPage();
- virtual QString displayName() const override
- {
- return tr("Version");
- }
+ virtual QString displayName() const override { return tr("Version"); }
virtual QIcon icon() const override;
- virtual QString id() const override
- {
- return "version";
- }
- virtual QString helpPage() const override
- {
- return "Instance-Version";
- }
+ virtual QString id() const override { return "version"; }
+ virtual QString helpPage() const override { return "Instance-Version"; }
virtual bool shouldDisplay() const override;
void retranslate() override;
void openedImpl() override;
void closedImpl() override;
-private slots:
+ private slots:
void on_actionChange_version_triggered();
void on_actionInstall_Forge_triggered();
void on_actionInstall_Fabric_triggered();
@@ -103,36 +92,34 @@ private slots:
void updateVersionControls();
-private:
+ private:
ComponentPtr current();
int currentRow();
void updateButtons(int row = -1);
void preselect(int row = 0);
int doUpdate();
-protected:
- QMenu * createPopupMenu() override;
+ protected:
+ QMenu* createPopupMenu() override;
/// FIXME: this shouldn't be necessary!
bool reloadPackProfile();
-private:
- Ui::VersionPage *ui;
- QSortFilterProxyModel *m_filterModel;
+ private:
+ Ui::VersionPage* ui;
+ QSortFilterProxyModel* m_filterModel;
std::shared_ptr<PackProfile> m_profile;
- MinecraftInstance *m_inst;
+ MinecraftInstance* m_inst;
int currentIdx = 0;
- bool controlsEnabled = false;
std::shared_ptr<Setting> m_wide_bar_setting = nullptr;
-public slots:
- void versionCurrent(const QModelIndex &current, const QModelIndex &previous);
+ public slots:
+ void versionCurrent(const QModelIndex& current, const QModelIndex& previous);
-private slots:
- void updateRunningStatus(bool running);
+ private slots:
void onGameUpdateError(QString error);
- void packageCurrent(const QModelIndex &current, const QModelIndex &previous);
- void showContextMenu(const QPoint &pos);
- void onFilterTextChanged(const QString & newContents);
+ void packageCurrent(const QModelIndex& current, const QModelIndex& previous);
+ void showContextMenu(const QPoint& pos);
+ void onFilterTextChanged(const QString& newContents);
};
diff --git a/launcher/ui/pages/instance/VersionPage.ui b/launcher/ui/pages/instance/VersionPage.ui
index 4777eafe..a73c42d6 100644
--- a/launcher/ui/pages/instance/VersionPage.ui
+++ b/launcher/ui/pages/instance/VersionPage.ui
@@ -102,7 +102,6 @@
<addaction name="actionInstall_Fabric"/>
<addaction name="actionInstall_Quilt"/>
<addaction name="actionInstall_LiteLoader"/>
- <addaction name="actionInstall_mods"/>
<addaction name="separator"/>
<addaction name="actionAdd_to_Minecraft_jar"/>
<addaction name="actionReplace_Minecraft_jar"/>
@@ -112,7 +111,6 @@
<addaction name="separator"/>
<addaction name="actionMinecraftFolder"/>
<addaction name="actionLibrariesFolder"/>
- <addaction name="separator"/>
<addaction name="actionReload"/>
<addaction name="actionDownload_All"/>
</widget>
@@ -204,14 +202,6 @@
<string>Install the LiteLoader package.</string>
</property>
</action>
- <action name="actionInstall_mods">
- <property name="text">
- <string>Install mods</string>
- </property>
- <property name="toolTip">
- <string>Install normal mods.</string>
- </property>
- </action>
<action name="actionAdd_to_Minecraft_jar">
<property name="text">
<string>Add to Minecraft.jar</string>
diff --git a/launcher/ui/pages/instance/WorldListPage.cpp b/launcher/ui/pages/instance/WorldListPage.cpp
index d4a395d9..b2200b1a 100644
--- a/launcher/ui/pages/instance/WorldListPage.cpp
+++ b/launcher/ui/pages/instance/WorldListPage.cpp
@@ -107,6 +107,7 @@ WorldListPage::WorldListPage(BaseInstance *inst, std::shared_ptr<WorldList> worl
auto head = ui->worldTreeView->header();
head->setSectionResizeMode(0, QHeaderView::Stretch);
head->setSectionResizeMode(1, QHeaderView::ResizeToContents);
+ head->setSectionResizeMode(4, QHeaderView::ResizeToContents);
connect(ui->worldTreeView->selectionModel(), &QItemSelectionModel::currentChanged, this, &WorldListPage::worldChanged);
worldChanged(QModelIndex(), QModelIndex());
@@ -338,6 +339,7 @@ void WorldListPage::mceditState(LoggedProcess::State state)
{
failed = true;
}
+ /* fallthrough */
case LoggedProcess::Running:
case LoggedProcess::Finished:
{
diff --git a/launcher/ui/pages/modplatform/VanillaPage.cpp b/launcher/ui/pages/modplatform/CustomPage.cpp
index 29fecb85..e164171a 100644
--- a/launcher/ui/pages/modplatform/VanillaPage.cpp
+++ b/launcher/ui/pages/modplatform/CustomPage.cpp
@@ -33,8 +33,8 @@
* limitations under the License.
*/
-#include "VanillaPage.h"
-#include "ui_VanillaPage.h"
+#include "CustomPage.h"
+#include "ui_CustomPage.h"
#include <QTabBar>
@@ -46,32 +46,32 @@
#include "minecraft/VanillaInstanceCreationTask.h"
#include "ui/dialogs/NewInstanceDialog.h"
-VanillaPage::VanillaPage(NewInstanceDialog *dialog, QWidget *parent)
- : QWidget(parent), dialog(dialog), ui(new Ui::VanillaPage)
+CustomPage::CustomPage(NewInstanceDialog *dialog, QWidget *parent)
+ : QWidget(parent), dialog(dialog), ui(new Ui::CustomPage)
{
ui->setupUi(this);
ui->tabWidget->tabBar()->hide();
- connect(ui->versionList, &VersionSelectWidget::selectedVersionChanged, this, &VanillaPage::setSelectedVersion);
+ connect(ui->versionList, &VersionSelectWidget::selectedVersionChanged, this, &CustomPage::setSelectedVersion);
filterChanged();
- connect(ui->alphaFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged);
- connect(ui->betaFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged);
- connect(ui->snapshotFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged);
- connect(ui->oldSnapshotFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged);
- connect(ui->releaseFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged);
- connect(ui->experimentsFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged);
- connect(ui->refreshBtn, &QPushButton::clicked, this, &VanillaPage::refresh);
-
- connect(ui->loaderVersionList, &VersionSelectWidget::selectedVersionChanged, this, &VanillaPage::setSelectedLoaderVersion);
- connect(ui->noneFilter, &QRadioButton::toggled, this, &VanillaPage::loaderFilterChanged);
- connect(ui->forgeFilter, &QRadioButton::toggled, this, &VanillaPage::loaderFilterChanged);
- connect(ui->fabricFilter, &QRadioButton::toggled, this, &VanillaPage::loaderFilterChanged);
- connect(ui->quiltFilter, &QRadioButton::toggled, this, &VanillaPage::loaderFilterChanged);
- connect(ui->liteLoaderFilter, &QRadioButton::toggled, this, &VanillaPage::loaderFilterChanged);
- connect(ui->loaderRefreshBtn, &QPushButton::clicked, this, &VanillaPage::loaderRefresh);
+ connect(ui->alphaFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged);
+ connect(ui->betaFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged);
+ connect(ui->snapshotFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged);
+ connect(ui->oldSnapshotFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged);
+ connect(ui->releaseFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged);
+ connect(ui->experimentsFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged);
+ connect(ui->refreshBtn, &QPushButton::clicked, this, &CustomPage::refresh);
+
+ connect(ui->loaderVersionList, &VersionSelectWidget::selectedVersionChanged, this, &CustomPage::setSelectedLoaderVersion);
+ connect(ui->noneFilter, &QRadioButton::toggled, this, &CustomPage::loaderFilterChanged);
+ connect(ui->forgeFilter, &QRadioButton::toggled, this, &CustomPage::loaderFilterChanged);
+ connect(ui->fabricFilter, &QRadioButton::toggled, this, &CustomPage::loaderFilterChanged);
+ connect(ui->quiltFilter, &QRadioButton::toggled, this, &CustomPage::loaderFilterChanged);
+ connect(ui->liteLoaderFilter, &QRadioButton::toggled, this, &CustomPage::loaderFilterChanged);
+ connect(ui->loaderRefreshBtn, &QPushButton::clicked, this, &CustomPage::loaderRefresh);
}
-void VanillaPage::openedImpl()
+void CustomPage::openedImpl()
{
if(!initialized)
{
@@ -85,19 +85,19 @@ void VanillaPage::openedImpl()
}
}
-void VanillaPage::refresh()
+void CustomPage::refresh()
{
ui->versionList->loadList();
}
-void VanillaPage::loaderRefresh()
+void CustomPage::loaderRefresh()
{
if(ui->noneFilter->isChecked())
return;
ui->loaderVersionList->loadList();
}
-void VanillaPage::filterChanged()
+void CustomPage::filterChanged()
{
QStringList out;
if(ui->alphaFilter->isChecked())
@@ -116,7 +116,7 @@ void VanillaPage::filterChanged()
ui->versionList->setFilter(BaseVersionList::TypeRole, new RegexpFilter(regexp, false));
}
-void VanillaPage::loaderFilterChanged()
+void CustomPage::loaderFilterChanged()
{
QString minecraftVersion;
if (m_selectedVersion)
@@ -172,37 +172,37 @@ void VanillaPage::loaderFilterChanged()
ui->loaderVersionList->setEmptyString(tr("No versions are currently available for Minecraft %1").arg(minecraftVersion));
}
-VanillaPage::~VanillaPage()
+CustomPage::~CustomPage()
{
delete ui;
}
-bool VanillaPage::shouldDisplay() const
+bool CustomPage::shouldDisplay() const
{
return true;
}
-void VanillaPage::retranslate()
+void CustomPage::retranslate()
{
ui->retranslateUi(this);
}
-BaseVersion::Ptr VanillaPage::selectedVersion() const
+BaseVersion::Ptr CustomPage::selectedVersion() const
{
return m_selectedVersion;
}
-BaseVersion::Ptr VanillaPage::selectedLoaderVersion() const
+BaseVersion::Ptr CustomPage::selectedLoaderVersion() const
{
return m_selectedLoaderVersion;
}
-QString VanillaPage::selectedLoader() const
+QString CustomPage::selectedLoader() const
{
return m_selectedLoader;
}
-void VanillaPage::suggestCurrent()
+void CustomPage::suggestCurrent()
{
if (!isOpened)
{
@@ -227,14 +227,14 @@ void VanillaPage::suggestCurrent()
dialog->setSuggestedIcon("default");
}
-void VanillaPage::setSelectedVersion(BaseVersion::Ptr version)
+void CustomPage::setSelectedVersion(BaseVersion::Ptr version)
{
m_selectedVersion = version;
suggestCurrent();
loaderFilterChanged();
}
-void VanillaPage::setSelectedLoaderVersion(BaseVersion::Ptr version)
+void CustomPage::setSelectedLoaderVersion(BaseVersion::Ptr version)
{
m_selectedLoaderVersion = version;
suggestCurrent();
diff --git a/launcher/ui/pages/modplatform/VanillaPage.h b/launcher/ui/pages/modplatform/CustomPage.h
index 39aba760..8b5a5011 100644
--- a/launcher/ui/pages/modplatform/VanillaPage.h
+++ b/launcher/ui/pages/modplatform/CustomPage.h
@@ -43,21 +43,21 @@
namespace Ui
{
-class VanillaPage;
+class CustomPage;
}
class NewInstanceDialog;
-class VanillaPage : public QWidget, public BasePage
+class CustomPage : public QWidget, public BasePage
{
Q_OBJECT
public:
- explicit VanillaPage(NewInstanceDialog *dialog, QWidget *parent = 0);
- virtual ~VanillaPage();
+ explicit CustomPage(NewInstanceDialog *dialog, QWidget *parent = 0);
+ virtual ~CustomPage();
virtual QString displayName() const override
{
- return tr("Vanilla");
+ return tr("Custom");
}
virtual QIcon icon() const override
{
@@ -96,7 +96,7 @@ private:
private:
bool initialized = false;
NewInstanceDialog *dialog = nullptr;
- Ui::VanillaPage *ui = nullptr;
+ Ui::CustomPage *ui = nullptr;
bool m_versionSetByUser = false;
BaseVersion::Ptr m_selectedVersion;
BaseVersion::Ptr m_selectedLoaderVersion;
diff --git a/launcher/ui/pages/modplatform/VanillaPage.ui b/launcher/ui/pages/modplatform/CustomPage.ui
index 43110927..0d89b595 100644
--- a/launcher/ui/pages/modplatform/VanillaPage.ui
+++ b/launcher/ui/pages/modplatform/CustomPage.ui
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
- <class>VanillaPage</class>
- <widget class="QWidget" name="VanillaPage">
+ <class>CustomPage</class>
+ <widget class="QWidget" name="CustomPage">
<property name="geometry">
<rect>
<x>0</x>
diff --git a/launcher/ui/pages/modplatform/ImportPage.h b/launcher/ui/pages/modplatform/ImportPage.h
index 8d13ac10..c2acb92d 100644
--- a/launcher/ui/pages/modplatform/ImportPage.h
+++ b/launcher/ui/pages/modplatform/ImportPage.h
@@ -57,7 +57,7 @@ public:
virtual ~ImportPage();
virtual QString displayName() const override
{
- return tr("Import from zip");
+ return tr("Import");
}
virtual QIcon icon() const override
{
diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp
index 3ffe6cb0..b7537890 100644
--- a/launcher/ui/pages/modplatform/ModModel.cpp
+++ b/launcher/ui/pages/modplatform/ModModel.cpp
@@ -6,12 +6,14 @@
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
+#include "minecraft/mod/ModFolderModel.h"
#include <QMessageBox>
+#include <algorithm>
namespace ResourceDownload {
-ModModel::ModModel(BaseInstance const& base_inst, ResourceAPI* api) : ResourceModel(api), m_base_instance(base_inst) {}
+ModModel::ModModel(BaseInstance& base_inst, ResourceAPI* api) : ResourceModel(api), m_base_instance(base_inst) {}
/******** Make data requests ********/
@@ -24,7 +26,7 @@ ResourceAPI::SearchArgs ModModel::createSearchArguments()
std::optional<std::list<Version>> versions{};
- { // Version filter
+ { // Version filter
if (!m_filter->versions.empty())
versions = m_filter->versions;
}
@@ -36,7 +38,7 @@ ResourceAPI::SearchArgs ModModel::createSearchArguments()
ResourceAPI::VersionSearchArgs ModModel::createVersionsArguments(QModelIndex& entry)
{
- auto& pack = m_packs[entry.row()];
+ auto& pack = *m_packs[entry.row()];
auto profile = static_cast<MinecraftInstance const&>(m_base_instance).getPackProfile();
Q_ASSERT(profile);
@@ -51,7 +53,7 @@ ResourceAPI::VersionSearchArgs ModModel::createVersionsArguments(QModelIndex& en
ResourceAPI::ProjectInfoArgs ModModel::createInfoArguments(QModelIndex& entry)
{
- auto& pack = m_packs[entry.row()];
+ auto& pack = *m_packs[entry.row()];
return { pack };
}
@@ -67,4 +69,14 @@ void ModModel::searchWithTerm(const QString& term, unsigned int sort, bool filte
refresh();
}
+bool ModModel::isPackInstalled(ModPlatform::IndexedPack::Ptr pack) const
+{
+ auto allMods = static_cast<MinecraftInstance&>(m_base_instance).loaderModList()->allMods();
+ return std::any_of(allMods.cbegin(), allMods.cend(), [pack](Mod* mod) {
+ if (auto meta = mod->metadata(); meta)
+ return meta->provider == pack->provider && meta->project_id == pack->addonId;
+ return false;
+ });
+}
+
} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/ModModel.h b/launcher/ui/pages/modplatform/ModModel.h
index 5d4a7785..dd187aa8 100644
--- a/launcher/ui/pages/modplatform/ModModel.h
+++ b/launcher/ui/pages/modplatform/ModModel.h
@@ -24,7 +24,7 @@ class ModModel : public ResourceModel {
Q_OBJECT
public:
- ModModel(const BaseInstance&, ResourceAPI* api);
+ ModModel(BaseInstance&, ResourceAPI* api);
/* Ask the API for more information */
void searchWithTerm(const QString& term, unsigned int sort, bool filter_changed);
@@ -32,6 +32,7 @@ class ModModel : public ResourceModel {
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;
+ virtual ModPlatform::IndexedVersion loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) = 0;
void setFilter(std::shared_ptr<ModFilterWidget::Filter> filter) { m_filter = filter; }
@@ -42,9 +43,10 @@ class ModModel : public ResourceModel {
protected:
auto documentToArray(QJsonDocument& obj) const -> QJsonArray override = 0;
+ virtual bool isPackInstalled(ModPlatform::IndexedPack::Ptr) const override;
protected:
- const BaseInstance& m_base_instance;
+ BaseInstance& m_base_instance;
std::shared_ptr<ModFilterWidget::Filter> m_filter = nullptr;
};
diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp
index 04be43ad..60a43128 100644
--- a/launcher/ui/pages/modplatform/ModPage.cpp
+++ b/launcher/ui/pages/modplatform/ModPage.cpp
@@ -55,8 +55,7 @@
namespace ResourceDownload {
-ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance& instance)
- : ResourcePage(dialog, instance)
+ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance& instance) : ResourcePage(dialog, instance)
{
connect(m_ui->searchButton, &QPushButton::clicked, this, &ModPage::triggerSearch);
connect(m_ui->resourceFilterButton, &QPushButton::clicked, this, &ModPage::filterMods);
@@ -75,12 +74,10 @@ void ModPage::setFilterWidget(unique_qobject_ptr<ModFilterWidget>& widget)
m_filter_widget->setInstance(&static_cast<MinecraftInstance&>(m_base_instance));
m_filter = m_filter_widget->getFilter();
- connect(m_filter_widget.get(), &ModFilterWidget::filterChanged, this, [&]{
- m_ui->searchButton->setStyleSheet("text-decoration: underline");
- });
- connect(m_filter_widget.get(), &ModFilterWidget::filterUnchanged, this, [&]{
- m_ui->searchButton->setStyleSheet("text-decoration: none");
- });
+ connect(m_filter_widget.get(), &ModFilterWidget::filterChanged, this,
+ [&] { m_ui->searchButton->setStyleSheet("text-decoration: underline"); });
+ connect(m_filter_widget.get(), &ModFilterWidget::filterUnchanged, this,
+ [&] { m_ui->searchButton->setStyleSheet("text-decoration: none"); });
}
/******** Callbacks to events in the UI (set up in the derived classes) ********/
@@ -92,17 +89,13 @@ void ModPage::filterMods()
void ModPage::triggerSearch()
{
- auto changed = m_filter_widget->changed();
m_filter = m_filter_widget->getFilter();
+ m_ui->packView->clearSelection();
+ m_ui->packDescription->clear();
+ m_ui->versionSelectionBox->clear();
+ updateSelectionButton();
- if (changed) {
- m_ui->packView->clearSelection();
- m_ui->packDescription->clear();
- m_ui->versionSelectionBox->clear();
- updateSelectionButton();
- }
-
- static_cast<ModModel*>(m_model)->searchWithTerm(getSearchTerm(), m_ui->sortByBox->currentData().toUInt(), changed);
+ static_cast<ModModel*>(m_model)->searchWithTerm(getSearchTerm(), m_ui->sortByBox->currentData().toUInt(), m_filter_widget->changed());
m_fetch_progress.watch(m_model->activeSearchJob().get());
}
@@ -125,11 +118,13 @@ void ModPage::updateVersionList()
QString mcVersion = packProfile->getComponentVersion("net.minecraft");
auto current_pack = getCurrentPack();
- for (int i = 0; i < current_pack.versions.size(); i++) {
- auto version = current_pack.versions[i];
+ if (!current_pack)
+ return;
+ 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.
+ for (auto& mcVer : m_filter->versions) {
+ // NOTE: Flame doesn't care about loader, so passing it changes nothing.
if (validateVersion(version, mcVer.toString(), packProfile->getModLoaders())) {
valid = true;
break;
@@ -148,10 +143,12 @@ void ModPage::updateVersionList()
updateSelectionButton();
}
-void ModPage::addResourceToDialog(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& version)
+void ModPage::addResourceToPage(ModPlatform::IndexedPack::Ptr pack,
+ ModPlatform::IndexedVersion& version,
+ const std::shared_ptr<ResourceFolderModel> base_model)
{
bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool();
- m_parent_dialog->addResource(pack, version, is_indexed);
+ m_model->addPack(pack, version, base_model, is_indexed);
}
} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/ModPage.h b/launcher/ui/pages/modplatform/ModPage.h
index c3b58cd6..5510c191 100644
--- a/launcher/ui/pages/modplatform/ModPage.h
+++ b/launcher/ui/pages/modplatform/ModPage.h
@@ -8,8 +8,8 @@
#include "modplatform/ModIndex.h"
-#include "ui/pages/modplatform/ResourcePage.h"
#include "ui/pages/modplatform/ModModel.h"
+#include "ui/pages/modplatform/ResourcePage.h"
#include "ui/widgets/ModFilterWidget.h"
namespace Ui {
@@ -25,13 +25,14 @@ class ModPage : public ResourcePage {
Q_OBJECT
public:
- template<typename T>
+ template <typename T>
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());
@@ -41,8 +42,6 @@ class ModPage : public ResourcePage {
return page;
}
- ~ModPage() override = default;
-
//: The plural version of 'mod'
[[nodiscard]] inline QString resourcesString() const override { return tr("mods"); }
//: The singular version of 'mods'
@@ -50,9 +49,13 @@ class ModPage : public ResourcePage {
[[nodiscard]] QMap<QString, QString> urlHandlers() const override;
- void addResourceToDialog(ModPlatform::IndexedPack&, ModPlatform::IndexedVersion&) override;
+ void addResourceToPage(ModPlatform::IndexedPack::Ptr,
+ ModPlatform::IndexedVersion&,
+ const std::shared_ptr<ResourceFolderModel>) override;
- virtual auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, std::optional<ResourceAPI::ModLoaderTypes> loaders = {}) const -> bool = 0;
+ virtual auto validateVersion(ModPlatform::IndexedVersion& ver,
+ QString mineVer,
+ std::optional<ResourceAPI::ModLoaderTypes> loaders = {}) const -> bool = 0;
[[nodiscard]] bool supportsFiltering() const override { return true; };
auto getFilter() const -> const std::shared_ptr<ModFilterWidget::Filter> { return m_filter; }
diff --git a/launcher/ui/pages/modplatform/ResourceModel.cpp b/launcher/ui/pages/modplatform/ResourceModel.cpp
index db7d26f8..49405a02 100644
--- a/launcher/ui/pages/modplatform/ResourceModel.cpp
+++ b/launcher/ui/pages/modplatform/ResourceModel.cpp
@@ -6,9 +6,12 @@
#include <QCryptographicHash>
#include <QIcon>
+#include <QList>
#include <QMessageBox>
#include <QPixmapCache>
#include <QUrl>
+#include <algorithm>
+#include <memory>
#include "Application.h"
#include "BuildConfig.h"
@@ -45,16 +48,16 @@ auto ResourceModel::data(const QModelIndex& index, int role) const -> QVariant
auto pack = m_packs.at(pos);
switch (role) {
case Qt::ToolTipRole: {
- if (pack.description.length() > 100) {
+ if (pack->description.length() > 100) {
// some magic to prevent to long tooltips and replace html linebreaks
- QString edit = pack.description.left(97);
+ QString edit = pack->description.left(97);
edit = edit.left(edit.lastIndexOf("<br>")).left(edit.lastIndexOf(" ")).append("...");
return edit;
}
- return pack.description;
+ return pack->description;
}
case Qt::DecorationRole: {
- if (auto icon_or_none = const_cast<ResourceModel*>(this)->getIcon(const_cast<QModelIndex&>(index), pack.logoUrl);
+ 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();
@@ -69,11 +72,13 @@ auto ResourceModel::data(const QModelIndex& index, int role) const -> QVariant
}
// Custom data
case UserDataTypes::TITLE:
- return pack.name;
+ return pack->name;
case UserDataTypes::DESCRIPTION:
- return pack.description;
+ return pack->description;
case UserDataTypes::SELECTED:
- return pack.isAnyVersionSelected();
+ return pack->isAnyVersionSelected();
+ case UserDataTypes::INSTALLED:
+ return this->isPackInstalled(pack);
default:
break;
}
@@ -92,6 +97,7 @@ QHash<int, QByteArray> ResourceModel::roleNames() const
roles[UserDataTypes::TITLE] = "title";
roles[UserDataTypes::DESCRIPTION] = "description";
roles[UserDataTypes::SELECTED] = "selected";
+ roles[UserDataTypes::INSTALLED] = "installed";
return roles;
}
@@ -102,7 +108,7 @@ bool ResourceModel::setData(const QModelIndex& index, const QVariant& value, int
if (pos >= m_packs.size() || pos < 0 || !index.isValid())
return false;
- m_packs[pos] = value.value<ModPlatform::IndexedPack>();
+ m_packs[pos] = value.value<ModPlatform::IndexedPack::Ptr>();
emit dataChanged(index, index);
return true;
@@ -161,7 +167,7 @@ void ResourceModel::loadEntry(QModelIndex& entry)
if (!hasActiveInfoJob())
m_current_info_job.clear();
- if (!pack.versionsLoaded) {
+ if (!pack->versionsLoaded) {
auto args{ createVersionsArguments(entry) };
auto callbacks{ createVersionsCallbacks(entry) };
@@ -177,7 +183,7 @@ void ResourceModel::loadEntry(QModelIndex& entry)
runInfoJob(job);
}
- if (!pack.extraDataLoaded) {
+ if (!pack->extraDataLoaded) {
auto args{ createInfoArguments(entry) };
auto callbacks{ createInfoCallbacks(entry) };
@@ -229,7 +235,7 @@ void ResourceModel::clearData()
void ResourceModel::runSearchJob(Task::Ptr ptr)
{
- m_current_search_job = ptr;
+ m_current_search_job.reset(ptr); // clean up first
m_current_search_job->start();
}
void ResourceModel::runInfoJob(Task::Ptr ptr)
@@ -326,16 +332,24 @@ void ResourceModel::loadIndexedPackVersions(ModPlatform::IndexedPack&, QJsonArra
void ResourceModel::searchRequestSucceeded(QJsonDocument& doc)
{
- QList<ModPlatform::IndexedPack> newList;
+ QList<ModPlatform::IndexedPack::Ptr> newList;
auto packs = documentToArray(doc);
for (auto packRaw : packs) {
auto packObj = packRaw.toObject();
- ModPlatform::IndexedPack pack;
+ ModPlatform::IndexedPack::Ptr pack = std::make_shared<ModPlatform::IndexedPack>();
try {
- loadIndexedPack(pack, packObj);
- newList.append(pack);
+ loadIndexedPack(*pack, packObj);
+ if (auto sel = std::find_if(m_selected.begin(), m_selected.end(),
+ [&pack](const DownloadTaskPtr i) {
+ const auto ipack = i->getPack();
+ return ipack->provider == pack->provider && ipack->addonId == pack->addonId;
+ });
+ sel != m_selected.end()) {
+ newList.append(sel->get()->getPack());
+ } else
+ newList.append(pack);
} catch (const JSONValidationError& e) {
qWarning() << "Error while loading resource from " << debugName() << ": " << e.cause();
continue;
@@ -389,15 +403,15 @@ void ResourceModel::searchRequestAborted()
void ResourceModel::versionRequestSucceeded(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index)
{
- auto current_pack = data(index, Qt::UserRole).value<ModPlatform::IndexedPack>();
+ auto current_pack = data(index, Qt::UserRole).value<ModPlatform::IndexedPack::Ptr>();
// Check if the index is still valid for this resource or not
- if (pack.addonId != current_pack.addonId)
+ if (pack.addonId != current_pack->addonId)
return;
try {
auto arr = doc.isObject() ? Json::ensureArray(doc.object(), "data") : doc.array();
- loadIndexedPackVersions(current_pack, arr);
+ loadIndexedPackVersions(*current_pack, arr);
} catch (const JSONValidationError& e) {
qDebug() << doc;
qWarning() << "Error while reading " << debugName() << " resource version: " << e.cause();
@@ -416,15 +430,15 @@ void ResourceModel::versionRequestSucceeded(QJsonDocument& doc, ModPlatform::Ind
void ResourceModel::infoRequestSucceeded(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index)
{
- auto current_pack = data(index, Qt::UserRole).value<ModPlatform::IndexedPack>();
+ auto current_pack = data(index, Qt::UserRole).value<ModPlatform::IndexedPack::Ptr>();
// Check if the index is still valid for this resource or not
- if (pack.addonId != current_pack.addonId)
+ if (pack.addonId != current_pack->addonId)
return;
try {
auto obj = Json::requireObject(doc);
- loadExtraPackInfo(current_pack, obj);
+ loadExtraPackInfo(*current_pack, obj);
} catch (const JSONValidationError& e) {
qDebug() << doc;
qWarning() << "Error while reading " << debugName() << " resource info: " << e.cause();
@@ -441,4 +455,39 @@ void ResourceModel::infoRequestSucceeded(QJsonDocument& doc, ModPlatform::Indexe
emit projectInfoUpdated();
}
+void ResourceModel::addPack(ModPlatform::IndexedPack::Ptr pack,
+ ModPlatform::IndexedVersion& version,
+ const std::shared_ptr<ResourceFolderModel> packs,
+ bool is_indexed,
+ QString custom_target_folder)
+{
+ version.is_currently_selected = true;
+ m_selected.append(makeShared<ResourceDownloadTask>(pack, version, packs, is_indexed, custom_target_folder));
+}
+
+void ResourceModel::removePack(const QString& rem)
+{
+ auto pred = [&rem](const DownloadTaskPtr i) { return rem == i->getName(); };
+#if QT_VERSION >= QT_VERSION_CHECK(6, 1, 0)
+ m_selected.removeIf(pred);
+#else
+ {
+ for (auto it = m_selected.begin(); it != m_selected.end();)
+ if (pred(*it))
+ it = m_selected.erase(it);
+ else
+ ++it;
+ }
+#endif
+ auto pack = std::find_if(m_packs.begin(), m_packs.end(), [&rem](const ModPlatform::IndexedPack::Ptr i) { return rem == i->name; });
+ if (pack == m_packs.end()) { // ignore it if is not in the current search
+ return;
+ }
+ if (!pack->get()->versionsLoaded) {
+ return;
+ }
+ for (auto& ver : pack->get()->versions)
+ ver.is_currently_selected = false;
+}
+
} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/ResourceModel.h b/launcher/ui/pages/modplatform/ResourceModel.h
index 46a02d6e..6533d9c6 100644
--- a/launcher/ui/pages/modplatform/ResourceModel.h
+++ b/launcher/ui/pages/modplatform/ResourceModel.h
@@ -10,6 +10,7 @@
#include "QObjectPtr.h"
+#include "ResourceDownloadTask.h"
#include "modplatform/ResourceAPI.h"
#include "tasks/ConcurrentTask.h"
@@ -29,6 +30,8 @@ class ResourceModel : public QAbstractListModel {
Q_PROPERTY(QString search_term MEMBER m_search_term WRITE setSearchTerm)
public:
+ using DownloadTaskPtr = shared_qobject_ptr<ResourceDownloadTask>;
+
ResourceModel(ResourceAPI* api);
~ResourceModel() override;
@@ -80,6 +83,14 @@ class ResourceModel : public QAbstractListModel {
/** 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&);
+ void addPack(ModPlatform::IndexedPack::Ptr pack,
+ ModPlatform::IndexedVersion& version,
+ const std::shared_ptr<ResourceFolderModel> packs,
+ bool is_indexed = false,
+ QString custom_target_folder = {});
+ void removePack(const QString& rem);
+ QList<DownloadTaskPtr> selectedPacks() { return m_selected; }
+
protected:
/** Resets the model's data. */
void clearData();
@@ -105,6 +116,8 @@ class ResourceModel : public QAbstractListModel {
virtual void loadExtraPackInfo(ModPlatform::IndexedPack&, QJsonObject&);
virtual void loadIndexedPackVersions(ModPlatform::IndexedPack&, QJsonArray&);
+ virtual bool isPackInstalled(ModPlatform::IndexedPack::Ptr) const { return false; }
+
protected:
/* Basic search parameters */
enum class SearchState { None, CanFetchMore, ResetRequested, Finished } m_search_state = SearchState::None;
@@ -123,7 +136,8 @@ class ResourceModel : public QAbstractListModel {
QSet<QUrl> m_currently_running_icon_actions;
QSet<QUrl> m_failed_icon_actions;
- QList<ModPlatform::IndexedPack> m_packs;
+ QList<ModPlatform::IndexedPack::Ptr> m_packs;
+ QList<DownloadTaskPtr> m_selected;
// 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?
diff --git a/launcher/ui/pages/modplatform/ResourcePackModel.cpp b/launcher/ui/pages/modplatform/ResourcePackModel.cpp
index 3df9a787..18c14bf8 100644
--- a/launcher/ui/pages/modplatform/ResourcePackModel.cpp
+++ b/launcher/ui/pages/modplatform/ResourcePackModel.cpp
@@ -22,13 +22,13 @@ ResourceAPI::SearchArgs ResourcePackResourceModel::createSearchArguments()
ResourceAPI::VersionSearchArgs ResourcePackResourceModel::createVersionsArguments(QModelIndex& entry)
{
auto& pack = m_packs[entry.row()];
- return { pack };
+ return { *pack };
}
ResourceAPI::ProjectInfoArgs ResourcePackResourceModel::createInfoArguments(QModelIndex& entry)
{
auto& pack = m_packs[entry.row()];
- return { pack };
+ return { *pack };
}
void ResourcePackResourceModel::searchWithTerm(const QString& term, unsigned int sort)
diff --git a/launcher/ui/pages/modplatform/ResourcePackPage.h b/launcher/ui/pages/modplatform/ResourcePackPage.h
index c01c89c4..8c5cf08b 100644
--- a/launcher/ui/pages/modplatform/ResourcePackPage.h
+++ b/launcher/ui/pages/modplatform/ResourcePackPage.h
@@ -31,8 +31,6 @@ class ResourcePackResourcePage : public ResourcePage {
return page;
}
- ~ResourcePackResourcePage() override = default;
-
//: The plural version of 'resource pack'
[[nodiscard]] inline QString resourcesString() const override { return tr("resource packs"); }
//: The singular version of 'resource packs'
diff --git a/launcher/ui/pages/modplatform/ResourcePage.cpp b/launcher/ui/pages/modplatform/ResourcePage.cpp
index bbd465bc..48afbd90 100644
--- a/launcher/ui/pages/modplatform/ResourcePage.cpp
+++ b/launcher/ui/pages/modplatform/ResourcePage.cpp
@@ -37,6 +37,7 @@
*/
#include "ResourcePage.h"
+#include "modplatform/ModIndex.h"
#include "ui_ResourcePage.h"
#include <QDesktopServices>
@@ -83,6 +84,8 @@ ResourcePage::ResourcePage(ResourceDownloadDialog* parent, BaseInstance& base_in
ResourcePage::~ResourcePage()
{
delete m_ui;
+ if (m_model)
+ delete m_model;
}
void ResourcePage::retranslate()
@@ -101,6 +104,7 @@ void ResourcePage::openedImpl()
updateSelectionButton();
triggerSearch();
+ m_ui->searchEdit->setFocus();
}
auto ResourcePage::eventFilter(QObject* watched, QEvent* event) -> bool
@@ -156,31 +160,35 @@ void ResourcePage::addSortings()
m_ui->sortByBox->addItem(sorting.readable_name, QVariant(sorting.index));
}
-bool ResourcePage::setCurrentPack(ModPlatform::IndexedPack pack)
+bool ResourcePage::setCurrentPack(ModPlatform::IndexedPack::Ptr pack)
{
QVariant v;
v.setValue(pack);
return m_model->setData(m_ui->packView->currentIndex(), v, Qt::UserRole);
}
-ModPlatform::IndexedPack ResourcePage::getCurrentPack() const
+ModPlatform::IndexedPack::Ptr ResourcePage::getCurrentPack() const
{
- return m_model->data(m_ui->packView->currentIndex(), Qt::UserRole).value<ModPlatform::IndexedPack>();
+ return m_model->data(m_ui->packView->currentIndex(), Qt::UserRole).value<ModPlatform::IndexedPack::Ptr>();
}
void ResourcePage::updateUi()
{
auto current_pack = getCurrentPack();
-
+ if (!current_pack) {
+ m_ui->packDescription->setHtml({});
+ m_ui->packDescription->flush();
+ return;
+ }
QString text = "";
- QString name = current_pack.name;
+ QString name = current_pack->name;
- if (current_pack.websiteUrl.isEmpty())
+ if (current_pack->websiteUrl.isEmpty())
text = name;
else
- text = "<a href=\"" + current_pack.websiteUrl + "\">" + name + "</a>";
+ text = "<a href=\"" + current_pack->websiteUrl + "\">" + name + "</a>";
- if (!current_pack.authors.empty()) {
+ if (!current_pack->authors.empty()) {
auto authorToStr = [](ModPlatform::ModpackAuthor& author) -> QString {
if (author.url.isEmpty()) {
return author.name;
@@ -188,44 +196,44 @@ void ResourcePage::updateUi()
return QString("<a href=\"%1\">%2</a>").arg(author.url, author.name);
};
QStringList authorStrs;
- for (auto& author : current_pack.authors) {
+ 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()) {
+ 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) {
+ 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()) {
+ 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>";
+ 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)));
+ text + (current_pack->extraData.body.isEmpty() ? current_pack->description : markdownToHTML(current_pack->extraData.body)));
m_ui->packDescription->flush();
}
@@ -237,10 +245,13 @@ void ResourcePage::updateSelectionButton()
}
m_ui->resourceSelectionButton->setEnabled(true);
- if (!getCurrentPack().isVersionSelected(m_selected_version_index)) {
- m_ui->resourceSelectionButton->setText(tr("Select %1 for download").arg(resourceString()));
+ if (auto current_pack = getCurrentPack(); current_pack) {
+ if (!current_pack->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()));
} else {
- m_ui->resourceSelectionButton->setText(tr("Deselect %1 for download").arg(resourceString()));
+ qWarning() << "Tried to update the selected button but there is not a pack selected";
}
}
@@ -252,13 +263,14 @@ void ResourcePage::updateVersionList()
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;
+ if (current_pack)
+ 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));
- }
+ 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));
@@ -277,7 +289,7 @@ void ResourcePage::onSelectionChanged(QModelIndex curr, QModelIndex prev)
auto current_pack = getCurrentPack();
bool request_load = false;
- if (!current_pack.versionsLoaded) {
+ if (!current_pack || !current_pack->versionsLoaded) {
m_ui->resourceSelectionButton->setText(tr("Loading versions..."));
m_ui->resourceSelectionButton->setEnabled(false);
@@ -286,7 +298,7 @@ void ResourcePage::onSelectionChanged(QModelIndex curr, QModelIndex prev)
updateVersionList();
}
- if (!current_pack.extraDataLoaded)
+ if (current_pack && !current_pack->extraDataLoaded)
request_load = true;
if (request_load)
@@ -306,14 +318,26 @@ void ResourcePage::onVersionSelectionChanged(QString data)
updateSelectionButton();
}
-void ResourcePage::addResourceToDialog(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& version)
+void ResourcePage::addResourceToDialog(ModPlatform::IndexedPack::Ptr pack, ModPlatform::IndexedVersion& version)
{
m_parent_dialog->addResource(pack, version);
}
-void ResourcePage::removeResourceFromDialog(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& version)
+void ResourcePage::removeResourceFromDialog(const QString& pack_name)
+{
+ m_parent_dialog->removeResource(pack_name);
+}
+
+void ResourcePage::addResourceToPage(ModPlatform::IndexedPack::Ptr pack,
+ ModPlatform::IndexedVersion& ver,
+ const std::shared_ptr<ResourceFolderModel> base_model)
+{
+ m_model->addPack(pack, ver, base_model);
+}
+
+void ResourcePage::removeResourceFromPage(const QString& name)
{
- m_parent_dialog->removeResource(pack, version);
+ m_model->removePack(name);
}
void ResourcePage::onResourceSelected()
@@ -322,12 +346,12 @@ void ResourcePage::onResourceSelected()
return;
auto current_pack = getCurrentPack();
- if (!current_pack.versionsLoaded)
+ if (!current_pack || !current_pack->versionsLoaded)
return;
- auto& version = current_pack.versions[m_selected_version_index];
+ auto& version = current_pack->versions[m_selected_version_index];
if (version.is_currently_selected)
- removeResourceFromDialog(current_pack, version);
+ removeResourceFromDialog(current_pack->name);
else
addResourceToDialog(current_pack, version);
@@ -338,7 +362,7 @@ void ResourcePage::onResourceSelected()
updateSelectionButton();
/* Force redraw on the resource list when the selection changes */
- m_ui->packView->adjustSize();
+ m_ui->packView->repaint();
}
void ResourcePage::openUrl(const QUrl& url)
@@ -368,7 +392,7 @@ void ResourcePage::openUrl(const QUrl& url)
const QString slug = match.captured(1);
// ensure the user isn't opening the same mod
- if (slug != getCurrentPack().slug) {
+ if (auto current_pack = getCurrentPack(); current_pack && slug != current_pack->slug) {
m_parent_dialog->selectPage(page);
auto newPage = m_parent_dialog->getSelectedPage();
diff --git a/launcher/ui/pages/modplatform/ResourcePage.h b/launcher/ui/pages/modplatform/ResourcePage.h
index 1896d53e..b4a87f57 100644
--- a/launcher/ui/pages/modplatform/ResourcePage.h
+++ b/launcher/ui/pages/modplatform/ResourcePage.h
@@ -7,10 +7,12 @@
#include <QTimer>
#include <QWidget>
+#include "ResourceDownloadTask.h"
#include "modplatform/ModIndex.h"
#include "modplatform/ResourceAPI.h"
#include "ui/pages/BasePage.h"
+#include "ui/pages/modplatform/ResourceModel.h"
#include "ui/widgets/ProgressWidget.h"
namespace Ui {
@@ -27,6 +29,7 @@ class ResourceModel;
class ResourcePage : public QWidget, public BasePage {
Q_OBJECT
public:
+ using DownloadTaskPtr = shared_qobject_ptr<ResourceDownloadTask>;
~ResourcePage() override;
/* Affects what the user sees */
@@ -57,8 +60,8 @@ class ResourcePage : public QWidget, public BasePage {
/** Programatically set the term in the search bar. */
void setSearchTerm(QString);
- [[nodiscard]] bool setCurrentPack(ModPlatform::IndexedPack);
- [[nodiscard]] auto getCurrentPack() const -> ModPlatform::IndexedPack;
+ [[nodiscard]] bool setCurrentPack(ModPlatform::IndexedPack::Ptr);
+ [[nodiscard]] auto getCurrentPack() const -> ModPlatform::IndexedPack::Ptr;
[[nodiscard]] auto getDialog() const -> const ResourceDownloadDialog* { return m_parent_dialog; }
[[nodiscard]] auto getModel() const -> ResourceModel* { return m_model; }
@@ -72,12 +75,17 @@ class ResourcePage : public QWidget, public BasePage {
virtual void updateSelectionButton();
virtual void updateVersionList();
- virtual void addResourceToDialog(ModPlatform::IndexedPack&, ModPlatform::IndexedVersion&);
- virtual void removeResourceFromDialog(ModPlatform::IndexedPack&, ModPlatform::IndexedVersion&);
+ void addResourceToDialog(ModPlatform::IndexedPack::Ptr, ModPlatform::IndexedVersion&);
+ void removeResourceFromDialog(const QString& pack_name);
+ virtual void removeResourceFromPage(const QString& name);
+ virtual void addResourceToPage(ModPlatform::IndexedPack::Ptr, ModPlatform::IndexedVersion&, const std::shared_ptr<ResourceFolderModel>);
+
+ QList<DownloadTaskPtr> selectedPacks() { return m_model->selectedPacks(); }
+ bool hasSelectedPacks() { return !(m_model->selectedPacks().isEmpty()); }
protected slots:
virtual void triggerSearch() {}
-
+
void onSelectionChanged(QModelIndex first, QModelIndex second);
void onVersionSelectionChanged(QString data);
void onResourceSelected();
diff --git a/launcher/ui/pages/modplatform/ShaderPackModel.cpp b/launcher/ui/pages/modplatform/ShaderPackModel.cpp
index 2101b394..aabd3be6 100644
--- a/launcher/ui/pages/modplatform/ShaderPackModel.cpp
+++ b/launcher/ui/pages/modplatform/ShaderPackModel.cpp
@@ -22,13 +22,13 @@ ResourceAPI::SearchArgs ShaderPackResourceModel::createSearchArguments()
ResourceAPI::VersionSearchArgs ShaderPackResourceModel::createVersionsArguments(QModelIndex& entry)
{
auto& pack = m_packs[entry.row()];
- return { pack };
+ return { *pack };
}
ResourceAPI::ProjectInfoArgs ShaderPackResourceModel::createInfoArguments(QModelIndex& entry)
{
auto& pack = m_packs[entry.row()];
- return { pack };
+ return { *pack };
}
void ShaderPackResourceModel::searchWithTerm(const QString& term, unsigned int sort)
diff --git a/launcher/ui/pages/modplatform/ShaderPackPage.cpp b/launcher/ui/pages/modplatform/ShaderPackPage.cpp
index 251c07e7..fbf94e84 100644
--- a/launcher/ui/pages/modplatform/ShaderPackPage.cpp
+++ b/launcher/ui/pages/modplatform/ShaderPackPage.cpp
@@ -13,8 +13,7 @@
namespace ResourceDownload {
-ShaderPackResourcePage::ShaderPackResourcePage(ShaderPackDownloadDialog* dialog, BaseInstance& instance)
- : ResourcePage(dialog, instance)
+ShaderPackResourcePage::ShaderPackResourcePage(ShaderPackDownloadDialog* dialog, BaseInstance& instance) : ResourcePage(dialog, instance)
{
connect(m_ui->searchButton, &QPushButton::clicked, this, &ShaderPackResourcePage::triggerSearch);
connect(m_ui->packView, &QListView::doubleClicked, this, &ShaderPackResourcePage::onResourceSelected);
@@ -38,17 +37,20 @@ QMap<QString, QString> ShaderPackResourcePage::urlHandlers() const
{
QMap<QString, QString> map;
map.insert(QRegularExpression::anchoredPattern("(?:www\\.)?modrinth\\.com\\/shaders\\/([^\\/]+)\\/?"), "modrinth");
- map.insert(QRegularExpression::anchoredPattern("(?:www\\.)?curseforge\\.com\\/minecraft\\/customization\\/([^\\/]+)\\/?"), "curseforge");
+ map.insert(QRegularExpression::anchoredPattern("(?:www\\.)?curseforge\\.com\\/minecraft\\/customization\\/([^\\/]+)\\/?"),
+ "curseforge");
map.insert(QRegularExpression::anchoredPattern("minecraft\\.curseforge\\.com\\/projects\\/([^\\/]+)\\/?"), "curseforge");
return map;
}
-void ShaderPackResourcePage::addResourceToDialog(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& version)
+void ShaderPackResourcePage::addResourceToPage(ModPlatform::IndexedPack::Ptr pack,
+ ModPlatform::IndexedVersion& version,
+ const std::shared_ptr<ResourceFolderModel> base_model)
{
+ QString custom_target_folder;
if (version.loaders.contains(QStringLiteral("canvas")))
- version.custom_target_folder = QStringLiteral("resourcepacks");
-
- m_parent_dialog->addResource(pack, version);
+ custom_target_folder = QStringLiteral("resourcepacks");
+ m_model->addPack(pack, version, base_model, false, custom_target_folder);
}
} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/ShaderPackPage.h b/launcher/ui/pages/modplatform/ShaderPackPage.h
index 972419a8..fcf6d4a7 100644
--- a/launcher/ui/pages/modplatform/ShaderPackPage.h
+++ b/launcher/ui/pages/modplatform/ShaderPackPage.h
@@ -31,8 +31,6 @@ class ShaderPackResourcePage : public ResourcePage {
return page;
}
- ~ShaderPackResourcePage() override = default;
-
//: The plural version of 'shader pack'
[[nodiscard]] inline QString resourcesString() const override { return tr("shader packs"); }
//: The singular version of 'shader packs'
@@ -40,7 +38,9 @@ class ShaderPackResourcePage : public ResourcePage {
[[nodiscard]] bool supportsFiltering() const override { return false; };
- void addResourceToDialog(ModPlatform::IndexedPack&, ModPlatform::IndexedVersion&) override;
+ void addResourceToPage(ModPlatform::IndexedPack::Ptr,
+ ModPlatform::IndexedVersion&,
+ const std::shared_ptr<ResourceFolderModel>) override;
[[nodiscard]] QMap<QString, QString> urlHandlers() const override;
diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp
index 9ad26f47..c6b087d6 100644
--- a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp
+++ b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp
@@ -16,62 +16,49 @@
#include "AtlListModel.h"
-#include <BuildConfig.h>
#include <Application.h>
+#include <BuildConfig.h>
#include <Json.h>
namespace Atl {
-ListModel::ListModel(QObject *parent) : QAbstractListModel(parent)
-{
-}
+ListModel::ListModel(QObject* parent) : QAbstractListModel(parent) {}
-ListModel::~ListModel()
-{
-}
+ListModel::~ListModel() {}
-int ListModel::rowCount(const QModelIndex &parent) const
+int ListModel::rowCount(const QModelIndex& parent) const
{
return parent.isValid() ? 0 : modpacks.size();
}
-int ListModel::columnCount(const QModelIndex &parent) const
+int ListModel::columnCount(const QModelIndex& parent) const
{
return parent.isValid() ? 0 : 1;
}
-QVariant ListModel::data(const QModelIndex &index, int role) const
+QVariant ListModel::data(const QModelIndex& index, int role) const
{
int pos = index.row();
- if(pos >= modpacks.size() || pos < 0 || !index.isValid())
- {
+ if (pos >= modpacks.size() || pos < 0 || !index.isValid()) {
return QString("INVALID INDEX %1").arg(pos);
}
ATLauncher::IndexedPack pack = modpacks.at(pos);
- if(role == Qt::DisplayRole)
- {
+ if (role == Qt::DisplayRole) {
return pack.name;
- }
- else if (role == Qt::ToolTipRole)
- {
+ } else if (role == Qt::ToolTipRole) {
return pack.name;
- }
- else if(role == Qt::DecorationRole)
- {
- if(m_logoMap.contains(pack.safeName))
- {
+ } else if (role == Qt::DecorationRole) {
+ if (m_logoMap.contains(pack.safeName)) {
return (m_logoMap.value(pack.safeName));
}
auto icon = APPLICATION->getThemedIcon("atlauncher-placeholder");
auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/images/%1.png").arg(pack.safeName.toLower());
- ((ListModel *)this)->requestLogo(pack.safeName, url);
+ ((ListModel*)this)->requestLogo(pack.safeName, url);
return icon;
- }
- else if(role == Qt::UserRole)
- {
+ } else if (role == Qt::UserRole) {
QVariant v;
v.setValue(pack);
return v;
@@ -88,7 +75,7 @@ void ListModel::request()
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));
+ netJob->addNetAction(Net::Download::makeByteArray(QUrl(url), response));
jobPtr = netJob;
jobPtr->start();
@@ -101,36 +88,38 @@ void ListModel::requestFinished()
jobPtr.reset();
QJsonParseError parse_error;
- QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error);
- if(parse_error.error != QJsonParseError::NoError) {
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
+ if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from ATL at " << parse_error.offset << " reason: " << parse_error.errorString();
- qWarning() << response;
+ qWarning() << *response;
return;
}
QList<ATLauncher::IndexedPack> newList;
auto packs = doc.array();
- for(auto packRaw : packs) {
+ for (auto packRaw : packs) {
auto packObj = packRaw.toObject();
ATLauncher::IndexedPack pack;
try {
ATLauncher::loadIndexedPack(pack, packObj);
- }
- catch (const JSONValidationError &e) {
- qDebug() << QString::fromUtf8(response);
+ } catch (const JSONValidationError& e) {
+ qDebug() << QString::fromUtf8(*response);
qWarning() << "Error while reading pack manifest from ATLauncher: " << e.cause();
return;
}
// ignore packs without a published version
- if(pack.versions.length() == 0) continue;
+ if (pack.versions.length() == 0)
+ continue;
// only display public packs (for now)
- if(pack.type != ATLauncher::PackType::Public) continue;
+ if (pack.type != ATLauncher::PackType::Public)
+ continue;
// ignore "system" packs (Vanilla, Vanilla with Forge, etc)
- if(pack.system) continue;
+ if (pack.system)
+ continue;
newList.append(pack);
}
@@ -145,14 +134,12 @@ void ListModel::requestFailed(QString reason)
jobPtr.reset();
}
-void ListModel::getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback)
+void ListModel::getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback)
{
- if(m_logoMap.contains(logo))
- {
- callback(APPLICATION->metacache()->resolveEntry("ATLauncherPacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath());
- }
- else
- {
+ if (m_logoMap.contains(logo)) {
+ callback(
+ APPLICATION->metacache()->resolveEntry("ATLauncherPacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath());
+ } else {
requestLogo(logo, logoUrl);
}
}
@@ -168,36 +155,34 @@ 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].safeName == logo) {
- emit dataChanged(createIndex(i, 0), createIndex(i, 0), {Qt::DecorationRole});
+ for (int i = 0; i < modpacks.size(); i++) {
+ if (modpacks[i].safeName == logo) {
+ emit dataChanged(createIndex(i, 0), createIndex(i, 0), { Qt::DecorationRole });
}
}
}
void ListModel::requestLogo(QString file, QString url)
{
- if(m_loadingLogos.contains(file) || m_failedLogos.contains(file))
- {
+ if (m_loadingLogos.contains(file) || m_failedLogos.contains(file)) {
return;
}
MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("ATLauncherPacks", QString("logos/%1").arg(file.section(".", 0, 0)));
- NetJob *job = new NetJob(QString("ATLauncher Icon Download %1").arg(file), APPLICATION->network());
+ auto job = new NetJob(QString("ATLauncher Icon Download %1").arg(file), APPLICATION->network());
job->addNetAction(Net::Download::makeCached(QUrl(url), entry));
auto fullPath = entry->getFullPath();
- QObject::connect(job, &NetJob::succeeded, this, [this, file, fullPath]
- {
+ QObject::connect(job, &NetJob::succeeded, this, [this, file, fullPath, job] {
+ job->deleteLater();
emit logoLoaded(file, QIcon(fullPath));
- if(waitingCallbacks.contains(file))
- {
+ if (waitingCallbacks.contains(file)) {
waitingCallbacks.value(file)(fullPath);
}
});
- QObject::connect(job, &NetJob::failed, this, [this, file]
- {
+ QObject::connect(job, &NetJob::failed, this, [this, file, job] {
+ job->deleteLater();
emit logoFailed(file);
});
@@ -206,4 +191,4 @@ void ListModel::requestLogo(QString file, QString url)
m_loadingLogos.append(file);
}
-}
+} // namespace Atl
diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.h b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.h
index 2574c48d..ed1fdc9f 100644
--- a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.h
+++ b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.h
@@ -18,42 +18,41 @@
#include <QAbstractListModel>
-#include "net/NetJob.h"
-#include <QIcon>
#include <modplatform/atlauncher/ATLPackIndex.h>
+#include <QIcon>
+#include "net/NetJob.h"
namespace Atl {
typedef QMap<QString, QIcon> LogoMap;
typedef std::function<void(QString)> LogoCallback;
-class ListModel : public QAbstractListModel
-{
+class ListModel : public QAbstractListModel {
Q_OBJECT
-public:
- ListModel(QObject *parent);
+ public:
+ ListModel(QObject* parent);
virtual ~ListModel();
- int rowCount(const QModelIndex &parent) const override;
- int columnCount(const QModelIndex &parent) const override;
- QVariant data(const QModelIndex &index, int role) const override;
+ int rowCount(const QModelIndex& parent) const override;
+ int columnCount(const QModelIndex& parent) const override;
+ QVariant data(const QModelIndex& index, int role) const override;
void request();
- void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback);
+ void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback);
-private slots:
+ private slots:
void requestFinished();
void requestFailed(QString reason);
void logoFailed(QString logo);
void logoLoaded(QString logo, QIcon out);
-private:
+ private:
void requestLogo(QString file, QString url);
-private:
+ private:
QList<ATLauncher::IndexedPack> modpacks;
QStringList m_failedLogos;
@@ -62,7 +61,7 @@ private:
QMap<QString, LogoCallback> waitingCallbacks;
NetJob::Ptr jobPtr;
- QByteArray response;
+ std::shared_ptr<QByteArray> response = std::make_shared<QByteArray>();
};
-}
+} // namespace Atl
diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp
index cdb4532c..7b61daa7 100644
--- a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp
+++ b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp
@@ -152,7 +152,7 @@ Qt::ItemFlags AtlOptionalModListModel::flags(const QModelIndex &index) const {
void AtlOptionalModListModel::useShareCode(const QString& code) {
m_jobPtr.reset(new NetJob("Atl::Request", APPLICATION->network()));
auto url = QString(BuildConfig.ATL_API_BASE_URL + "share-codes/" + code);
- m_jobPtr->addNetAction(Net::Download::makeByteArray(QUrl(url), &m_response));
+ m_jobPtr->addNetAction(Net::Download::makeByteArray(QUrl(url), m_response));
connect(m_jobPtr.get(), &NetJob::succeeded,
this, &AtlOptionalModListModel::shareCodeSuccess);
@@ -166,10 +166,10 @@ void AtlOptionalModListModel::shareCodeSuccess() {
m_jobPtr.reset();
QJsonParseError parse_error {};
- auto doc = QJsonDocument::fromJson(m_response, &parse_error);
+ auto doc = QJsonDocument::fromJson(*m_response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from ATL at " << parse_error.offset << " reason: " << parse_error.errorString();
- qWarning() << m_response;
+ qWarning() << *m_response;
return;
}
auto obj = doc.object();
@@ -179,7 +179,7 @@ void AtlOptionalModListModel::shareCodeSuccess() {
ATLauncher::loadShareCodeResponse(response, obj);
}
catch (const JSONValidationError& e) {
- qDebug() << QString::fromUtf8(m_response);
+ qDebug() << QString::fromUtf8(*m_response);
qWarning() << "Error while reading response from ATLauncher: " << e.cause();
return;
}
diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h
index 8e02444e..639f0d48 100644
--- a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h
+++ b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h
@@ -82,9 +82,9 @@ private:
void toggleMod(ATLauncher::VersionMod mod, int index);
void setMod(ATLauncher::VersionMod mod, int index, bool enable, bool shouldEmit = true);
-private:
+ private:
NetJob::Ptr m_jobPtr;
- QByteArray m_response;
+ std::shared_ptr<QByteArray> m_response = std::make_shared<QByteArray>();
ATLauncher::PackVersion m_version;
QVector<ATLauncher::VersionMod> m_mods;
diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.cpp
index f5f50cae..3d2d568a 100644
--- a/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.cpp
+++ b/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.cpp
@@ -68,7 +68,7 @@ QString AtlUserInteractionSupportImpl::chooseVersion(Meta::VersionList::Ptr vlis
// select recommended build
for (int i = 0; i < vlist->versions().size(); i++) {
auto version = vlist->versions().at(i);
- auto reqs = version->requires();
+ auto reqs = version->requiredSet();
// filter by minecraft version, if the loader depends on a certain version.
if (minecraftVersion != nullptr) {
diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.h b/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.h
index 37010b3f..adeb53cb 100644
--- a/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.h
+++ b/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.h
@@ -42,15 +42,15 @@
class AtlUserInteractionSupportImpl : public QObject, public ATLauncher::UserInteractionSupport {
Q_OBJECT
-public:
+ public:
AtlUserInteractionSupportImpl(QWidget* parent);
+ virtual ~AtlUserInteractionSupportImpl() = default;
-private:
+ private:
QString chooseVersion(Meta::VersionList::Ptr vlist, QString minecraftVersion) override;
std::optional<QVector<QString>> chooseOptionalMods(ATLauncher::PackVersion version, QVector<ATLauncher::VersionMod> mods) override;
void displayMessage(QString message) override;
-private:
+ private:
QWidget* m_parent;
-
};
diff --git a/launcher/ui/pages/modplatform/flame/FlameModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModel.cpp
index 5961ea02..fa55aa68 100644
--- a/launcher/ui/pages/modplatform/flame/FlameModel.cpp
+++ b/launcher/ui/pages/modplatform/flame/FlameModel.cpp
@@ -60,6 +60,8 @@ QVariant ListModel::data(const QModelIndex& index, int role) const
return pack.description;
case UserDataTypes::SELECTED:
return false;
+ case UserDataTypes::INSTALLED:
+ return false;
default:
break;
}
@@ -169,7 +171,7 @@ void ListModel::performPaginatedSearch()
.arg(currentSearchTerm)
.arg(currentSort + 1);
- netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response));
+ netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), response));
jobPtr = netJob;
jobPtr->start();
QObject::connect(netJob.get(), &NetJob::succeeded, this, &ListModel::searchRequestFinished);
@@ -202,11 +204,11 @@ void Flame::ListModel::searchRequestFinished()
jobPtr.reset();
QJsonParseError parse_error;
- QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error);
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from CurseForge at " << parse_error.offset
<< " reason: " << parse_error.errorString();
- qWarning() << response;
+ qWarning() << *response;
return;
}
diff --git a/launcher/ui/pages/modplatform/flame/FlameModel.h b/launcher/ui/pages/modplatform/flame/FlameModel.h
index cab666cc..b3bc96b8 100644
--- a/launcher/ui/pages/modplatform/flame/FlameModel.h
+++ b/launcher/ui/pages/modplatform/flame/FlameModel.h
@@ -3,46 +3,44 @@
#include <RWStorage.h>
#include <QAbstractListModel>
-#include <QSortFilterProxyModel>
-#include <QThreadPool>
#include <QIcon>
-#include <QStyledItemDelegate>
#include <QList>
+#include <QMetaType>
+#include <QSortFilterProxyModel>
#include <QString>
#include <QStringList>
-#include <QMetaType>
+#include <QStyledItemDelegate>
+#include <QThreadPool>
-#include <functional>
#include <net/NetJob.h>
+#include <functional>
#include <modplatform/flame/FlamePackIndex.h>
namespace Flame {
-
typedef QMap<QString, QIcon> LogoMap;
typedef std::function<void(QString)> LogoCallback;
-class ListModel : public QAbstractListModel
-{
+class ListModel : public QAbstractListModel {
Q_OBJECT
-public:
- ListModel(QObject *parent);
+ public:
+ ListModel(QObject* parent);
virtual ~ListModel();
- int rowCount(const QModelIndex &parent) const override;
- int columnCount(const QModelIndex &parent) const override;
- QVariant data(const QModelIndex &index, int role) const override;
- bool setData(const QModelIndex &index, const QVariant &value, int role) override;
- Qt::ItemFlags flags(const QModelIndex &index) const override;
- bool canFetchMore(const QModelIndex & parent) const override;
- void fetchMore(const QModelIndex & parent) override;
+ int rowCount(const QModelIndex& parent) const override;
+ int columnCount(const QModelIndex& parent) const override;
+ QVariant data(const QModelIndex& index, int role) const override;
+ bool setData(const QModelIndex& index, const QVariant& value, int role) override;
+ Qt::ItemFlags flags(const QModelIndex& index) const override;
+ bool canFetchMore(const QModelIndex& parent) const override;
+ void fetchMore(const QModelIndex& parent) override;
- void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback);
- void searchWithTerm(const QString & term, const int sort);
+ void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback);
+ void searchWithTerm(const QString& term, const int sort);
-private slots:
+ private slots:
void performPaginatedSearch();
void logoFailed(QString logo);
@@ -51,10 +49,10 @@ private slots:
void searchRequestFinished();
void searchRequestFailed(QString reason);
-private:
+ private:
void requestLogo(QString file, QString url);
-private:
+ private:
QList<IndexedPack> modpacks;
QStringList m_failedLogos;
QStringList m_loadingLogos;
@@ -64,14 +62,9 @@ private:
QString currentSearchTerm;
int currentSort = 0;
int nextSearchOffset = 0;
- enum SearchState {
- None,
- CanPossiblyFetchMore,
- ResetRequested,
- Finished
- } searchState = None;
+ enum SearchState { None, CanPossiblyFetchMore, ResetRequested, Finished } searchState = None;
NetJob::Ptr jobPtr;
- QByteArray response;
+ std::shared_ptr<QByteArray> response = std::make_shared<QByteArray>();
};
-}
+} // namespace Flame
diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.cpp b/launcher/ui/pages/modplatform/flame/FlamePage.cpp
index f9ac4a78..cef26bb6 100644
--- a/launcher/ui/pages/modplatform/flame/FlamePage.cpp
+++ b/launcher/ui/pages/modplatform/flame/FlamePage.cpp
@@ -130,7 +130,7 @@ void FlamePage::onSelectionChanged(QModelIndex curr, QModelIndex prev)
if (current.versionsLoaded == false) {
qDebug() << "Loading flame modpack versions";
auto netJob = new NetJob(QString("Flame::PackVersions(%1)").arg(current.name), APPLICATION->network());
- auto response = new QByteArray();
+ auto response = std::make_shared<QByteArray>();
int addonId = current.addonId;
netJob->addNetAction(Net::Download::makeByteArray(QString("https://api.curseforge.com/v1/mods/%1/files").arg(addonId), response));
@@ -170,10 +170,7 @@ void FlamePage::onSelectionChanged(QModelIndex curr, QModelIndex prev)
}
suggestCurrent();
});
- QObject::connect(netJob, &NetJob::finished, this, [response, netJob] {
- netJob->deleteLater();
- delete response;
- });
+ QObject::connect(netJob, &NetJob::finished, this, [response, netJob] { netJob->deleteLater(); });
netJob->start();
} else {
for (auto version : current.versions) {
diff --git a/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp b/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp
index e3d0bc14..0fb67c50 100644
--- a/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp
+++ b/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp
@@ -11,7 +11,7 @@
namespace ResourceDownload {
-FlameModModel::FlameModModel(BaseInstance const& base) : ModModel(base, new FlameAPI) {}
+FlameModModel::FlameModModel(BaseInstance& base) : ModModel(base, new FlameAPI) {}
void FlameModModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj)
{
@@ -29,6 +29,11 @@ void FlameModModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonAr
FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance);
}
+auto FlameModModel::loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion
+{
+ return FlameMod::loadDependencyVersions(m, arr);
+};
+
auto FlameModModel::documentToArray(QJsonDocument& obj) const -> QJsonArray
{
return Json::ensureArray(obj.object(), "data");
@@ -81,7 +86,7 @@ void FlameTexturePackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m,
auto const& mc_versions = version.mcVersion;
if (std::any_of(mc_versions.constBegin(), mc_versions.constEnd(),
- [this](auto const& mc_version){ return Version(mc_version) <= maximumTexturePackVersion(); }))
+ [this](auto const& mc_version) { return Version(mc_version) <= maximumTexturePackVersion(); }))
filtered_versions.push_back(version);
}
diff --git a/launcher/ui/pages/modplatform/flame/FlameResourceModels.h b/launcher/ui/pages/modplatform/flame/FlameResourceModels.h
index 0252ac40..6cfd6a6f 100644
--- a/launcher/ui/pages/modplatform/flame/FlameResourceModels.h
+++ b/launcher/ui/pages/modplatform/flame/FlameResourceModels.h
@@ -14,7 +14,7 @@ class FlameModModel : public ModModel {
Q_OBJECT
public:
- FlameModModel(const BaseInstance&);
+ FlameModModel(BaseInstance&);
~FlameModModel() override = default;
private:
@@ -24,6 +24,7 @@ class FlameModModel : public ModModel {
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 loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion override;
auto documentToArray(QJsonDocument& obj) const -> QJsonArray override;
};
diff --git a/launcher/ui/pages/modplatform/ftb/FtbFilterModel.cpp b/launcher/ui/pages/modplatform/ftb/FtbFilterModel.cpp
deleted file mode 100644
index e2b548f2..00000000
--- a/launcher/ui/pages/modplatform/ftb/FtbFilterModel.cpp
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org>
- *
- * 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 "FtbFilterModel.h"
-
-#include <QDebug>
-
-#include "modplatform/modpacksch/FTBPackManifest.h"
-
-#include "StringUtils.h"
-
-namespace Ftb {
-
-FilterModel::FilterModel(QObject *parent) : QSortFilterProxyModel(parent)
-{
- currentSorting = Sorting::ByPlays;
- sortings.insert(tr("Sort by Plays"), Sorting::ByPlays);
- sortings.insert(tr("Sort by Installs"), Sorting::ByInstalls);
- sortings.insert(tr("Sort by Name"), Sorting::ByName);
-}
-
-const QMap<QString, FilterModel::Sorting> FilterModel::getAvailableSortings()
-{
- return sortings;
-}
-
-QString FilterModel::translateCurrentSorting()
-{
- return sortings.key(currentSorting);
-}
-
-void FilterModel::setSorting(Sorting sorting)
-{
- currentSorting = sorting;
- invalidate();
-}
-
-FilterModel::Sorting FilterModel::getCurrentSorting()
-{
- return currentSorting;
-}
-
-void FilterModel::setSearchTerm(const QString& term)
-{
- searchTerm = term.trimmed();
- invalidate();
-}
-
-bool FilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
-{
- if (searchTerm.isEmpty()) {
- return true;
- }
-
- auto index = sourceModel()->index(sourceRow, 0, sourceParent);
- auto pack = sourceModel()->data(index, Qt::UserRole).value<ModpacksCH::Modpack>();
- return pack.name.contains(searchTerm, Qt::CaseInsensitive);
-}
-
-bool FilterModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
-{
- ModpacksCH::Modpack leftPack = sourceModel()->data(left, Qt::UserRole).value<ModpacksCH::Modpack>();
- ModpacksCH::Modpack rightPack = sourceModel()->data(right, Qt::UserRole).value<ModpacksCH::Modpack>();
-
- if (currentSorting == ByPlays) {
- return leftPack.plays < rightPack.plays;
- }
- else if (currentSorting == ByInstalls) {
- return leftPack.installs < rightPack.installs;
- }
- else if (currentSorting == ByName) {
- return StringUtils::naturalCompare(leftPack.name, rightPack.name, Qt::CaseSensitive) >= 0;
- }
-
- // Invalid sorting set, somehow...
- qWarning() << "Invalid sorting set!";
- return true;
-}
-
-}
diff --git a/launcher/ui/pages/modplatform/ftb/FtbFilterModel.h b/launcher/ui/pages/modplatform/ftb/FtbFilterModel.h
deleted file mode 100644
index 1be28e99..00000000
--- a/launcher/ui/pages/modplatform/ftb/FtbFilterModel.h
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org>
- *
- * 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 <QtCore/QSortFilterProxyModel>
-
-namespace Ftb {
-
-class FilterModel : public QSortFilterProxyModel
-{
- Q_OBJECT
-
-public:
- FilterModel(QObject* parent = Q_NULLPTR);
- enum Sorting {
- ByPlays,
- ByInstalls,
- ByName,
- };
- const QMap<QString, Sorting> getAvailableSortings();
- QString translateCurrentSorting();
- void setSorting(Sorting sorting);
- Sorting getCurrentSorting();
- void setSearchTerm(const QString& term);
-
-protected:
- bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
- bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;
-
-private:
- QMap<QString, Sorting> sortings;
- Sorting currentSorting;
- QString searchTerm { "" };
-
-};
-
-}
diff --git a/launcher/ui/pages/modplatform/ftb/FtbListModel.cpp b/launcher/ui/pages/modplatform/ftb/FtbListModel.cpp
deleted file mode 100644
index e8065415..00000000
--- a/launcher/ui/pages/modplatform/ftb/FtbListModel.cpp
+++ /dev/null
@@ -1,304 +0,0 @@
-/*
- * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org>
- *
- * 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 "FtbListModel.h"
-
-#include "BuildConfig.h"
-#include "Application.h"
-#include "Json.h"
-
-#include <QPainter>
-
-namespace Ftb {
-
-ListModel::ListModel(QObject *parent) : QAbstractListModel(parent)
-{
-}
-
-ListModel::~ListModel()
-{
-}
-
-int ListModel::rowCount(const QModelIndex &parent) const
-{
- return parent.isValid() ? 0 : modpacks.size();
-}
-
-int ListModel::columnCount(const QModelIndex &parent) const
-{
- return parent.isValid() ? 0 : 1;
-}
-
-QVariant ListModel::data(const QModelIndex &index, int role) const
-{
- int pos = index.row();
- if(pos >= modpacks.size() || pos < 0 || !index.isValid())
- {
- return QString("INVALID INDEX %1").arg(pos);
- }
-
- ModpacksCH::Modpack pack = modpacks.at(pos);
- if(role == Qt::DisplayRole)
- {
- return pack.name;
- }
- else if (role == Qt::ToolTipRole)
- {
- return pack.synopsis;
- }
- else if(role == Qt::DecorationRole)
- {
- QIcon placeholder = APPLICATION->getThemedIcon("screenshot-placeholder");
-
- auto iter = m_logoMap.find(pack.name);
- if (iter != m_logoMap.end()) {
- auto & logo = *iter;
- if(!logo.result.isNull()) {
- return logo.result;
- }
- return placeholder;
- }
-
- for(auto art : pack.art) {
- if(art.type == "square") {
- ((ListModel *)this)->requestLogo(pack.name, art.url);
- }
- }
- return placeholder;
- }
- else if(role == Qt::UserRole)
- {
- QVariant v;
- v.setValue(pack);
- return v;
- }
-
- return QVariant();
-}
-
-void ListModel::getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback)
-{
- if(m_logoMap.contains(logo))
- {
- callback(APPLICATION->metacache()->resolveEntry("ModpacksCHPacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath());
- }
- else
- {
- requestLogo(logo, logoUrl);
- }
-}
-
-void ListModel::request()
-{
- m_aborted = false;
-
- beginResetModel();
- modpacks.clear();
- endResetModel();
-
- 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.get(), &NetJob::succeeded, this, &ListModel::requestFinished);
- QObject::connect(netJob.get(), &NetJob::failed, this, &ListModel::requestFailed);
-}
-
-void ListModel::abortRequest()
-{
- m_aborted = jobPtr->abort();
- jobPtr.reset();
-}
-
-void ListModel::requestFinished()
-{
- jobPtr.reset();
- remainingPacks.clear();
-
- QJsonParseError parse_error {};
- QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error);
- if(parse_error.error != QJsonParseError::NoError) {
- qWarning() << "Error while parsing JSON response from ModpacksCH at " << parse_error.offset << " reason: " << parse_error.errorString();
- qWarning() << response;
- return;
- }
-
- auto packs = doc.object().value("packs").toArray();
- for(auto pack : packs) {
- auto packId = pack.toInt();
- remainingPacks.append(packId);
- }
-
- if(!remainingPacks.isEmpty()) {
- currentPack = remainingPacks.at(0);
- requestPack();
- }
-}
-
-void ListModel::requestFailed(QString reason)
-{
- jobPtr.reset();
- remainingPacks.clear();
-}
-
-void ListModel::requestPack()
-{
- 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.get(), &NetJob::succeeded, this, &ListModel::packRequestFinished);
- QObject::connect(netJob.get(), &NetJob::failed, this, &ListModel::packRequestFailed);
-}
-
-void ListModel::packRequestFinished()
-{
- if (!jobPtr || m_aborted)
- return;
-
- jobPtr.reset();
- remainingPacks.removeOne(currentPack);
-
- QJsonParseError parse_error;
- QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error);
-
- if(parse_error.error != QJsonParseError::NoError) {
- qWarning() << "Error while parsing JSON response from ModpacksCH at " << parse_error.offset << " reason: " << parse_error.errorString();
- qWarning() << response;
- return;
- }
-
- auto obj = doc.object();
-
- ModpacksCH::Modpack pack;
- try
- {
- ModpacksCH::loadModpack(pack, obj);
- }
- catch (const JSONValidationError &e)
- {
- qDebug() << QString::fromUtf8(response);
- qWarning() << "Error while reading pack manifest from ModpacksCH: " << e.cause();
- return;
- }
-
- // Since there is no guarantee that packs have a version, this will just
- // ignore those "dud" packs.
- if (pack.versions.empty())
- {
- qWarning() << "ModpacksCH Pack " << pack.id << " ignored. reason: lacking any versions";
- }
- else
- {
- beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size());
- modpacks.append(pack);
- endInsertRows();
- }
-
- if(!remainingPacks.isEmpty()) {
- currentPack = remainingPacks.at(0);
- requestPack();
- }
-}
-
-void ListModel::packRequestFailed(QString reason)
-{
- jobPtr.reset();
- remainingPacks.removeOne(currentPack);
-}
-
-void ListModel::logoLoaded(QString logo, bool stale)
-{
- auto & logoObj = m_logoMap[logo];
- logoObj.downloadJob.reset();
- QString smallPath = logoObj.fullpath + ".small";
-
- QFileInfo smallInfo(smallPath);
-
- if(stale || !smallInfo.exists()) {
- QImage image(logoObj.fullpath);
- if (image.isNull())
- {
- logoObj.failed = true;
- return;
- }
- QImage small;
- if (image.width() > image.height()) {
- small = image.scaledToWidth(512).scaledToWidth(256, Qt::SmoothTransformation);
- }
- else {
- small = image.scaledToHeight(512).scaledToHeight(256, Qt::SmoothTransformation);
- }
- QPoint offset((256 - small.width()) / 2, (256 - small.height()) / 2);
- QImage square(QSize(256, 256), QImage::Format_ARGB32);
- square.fill(Qt::transparent);
-
- QPainter painter(&square);
- painter.drawImage(offset, small);
- painter.end();
-
- square.save(logoObj.fullpath + ".small", "PNG");
- }
-
- logoObj.result = QIcon(logoObj.fullpath + ".small");
- for(int i = 0; i < modpacks.size(); i++) {
- if(modpacks[i].name == logo) {
- emit dataChanged(createIndex(i, 0), createIndex(i, 0), {Qt::DecorationRole});
- }
- }
-}
-
-void ListModel::logoFailed(QString logo)
-{
- m_logoMap[logo].failed = true;
- m_logoMap[logo].downloadJob.reset();
-}
-
-void ListModel::requestLogo(QString logo, QString url)
-{
- if(m_logoMap.contains(logo)) {
- return;
- }
-
- MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("ModpacksCHPacks", QString("logos/%1").arg(logo.section(".", 0, 0)));
-
- bool stale = entry->isStale();
-
- 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.get(), &NetJob::finished, this, [this, logo, fullPath, stale]
- {
- logoLoaded(logo, stale);
- });
-
- QObject::connect(job.get(), &NetJob::failed, this, [this, logo]
- {
- logoFailed(logo);
- });
-
- auto &newLogoEntry = m_logoMap[logo];
- newLogoEntry.downloadJob = job;
- newLogoEntry.fullpath = fullPath;
- job->start();
-}
-
-}
diff --git a/launcher/ui/pages/modplatform/ftb/FtbListModel.h b/launcher/ui/pages/modplatform/ftb/FtbListModel.h
deleted file mode 100644
index d7a120f0..00000000
--- a/launcher/ui/pages/modplatform/ftb/FtbListModel.h
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org>
- *
- * 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 <QAbstractListModel>
-
-#include "modplatform/modpacksch/FTBPackManifest.h"
-#include "net/NetJob.h"
-#include <QIcon>
-
-namespace Ftb {
-
-struct Logo {
- QString fullpath;
- NetJob::Ptr downloadJob;
- QIcon result;
- bool failed = false;
-};
-
-typedef QMap<QString, Logo> LogoMap;
-typedef std::function<void(QString)> LogoCallback;
-
-class ListModel : public QAbstractListModel
-{
- Q_OBJECT
-
-public:
- ListModel(QObject *parent);
- virtual ~ListModel();
-
- int rowCount(const QModelIndex &parent) const override;
- int columnCount(const QModelIndex &parent) const override;
- QVariant data(const QModelIndex &index, int role) const override;
-
- void request();
- void abortRequest();
-
- void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback);
-
- [[nodiscard]] bool isMakingRequest() const { return jobPtr.get(); }
- [[nodiscard]] bool wasAborted() const { return m_aborted; }
-
-private slots:
- void requestFinished();
- void requestFailed(QString reason);
-
- void requestPack();
- void packRequestFinished();
- void packRequestFailed(QString reason);
-
- void logoFailed(QString logo);
- void logoLoaded(QString logo, bool stale);
-
-private:
- void requestLogo(QString file, QString url);
-
-private:
- bool m_aborted = false;
-
- QList<ModpacksCH::Modpack> modpacks;
- LogoMap m_logoMap;
-
- NetJob::Ptr jobPtr;
- int currentPack;
- QList<int> remainingPacks;
- QByteArray response;
-};
-
-}
diff --git a/launcher/ui/pages/modplatform/ftb/FtbPage.cpp b/launcher/ui/pages/modplatform/ftb/FtbPage.cpp
deleted file mode 100644
index 7d59a6ae..00000000
--- a/launcher/ui/pages/modplatform/ftb/FtbPage.cpp
+++ /dev/null
@@ -1,199 +0,0 @@
-// SPDX-License-Identifier: GPL-3.0-only
-/*
- * PolyMC - Minecraft Launcher
- * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
- * 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 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org>
- * Copyright 2021 Philip T <me@phit.link>
- *
- * 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 "FtbPage.h"
-#include "ui_FtbPage.h"
-
-#include <QKeyEvent>
-
-#include "ui/dialogs/NewInstanceDialog.h"
-#include "modplatform/modpacksch/FTBPackInstallTask.h"
-
-#include "Markdown.h"
-
-FtbPage::FtbPage(NewInstanceDialog* dialog, QWidget *parent)
- : QWidget(parent), ui(new Ui::FtbPage), dialog(dialog)
-{
- ui->setupUi(this);
-
- filterModel = new Ftb::FilterModel(this);
- listModel = new Ftb::ListModel(this);
- filterModel->setSourceModel(listModel);
- ui->packView->setModel(filterModel);
- ui->packView->setSortingEnabled(true);
- ui->packView->header()->hide();
- ui->packView->setIndentation(0);
-
- ui->searchEdit->installEventFilter(this);
-
- ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
- ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300);
-
- for(int i = 0; i < filterModel->getAvailableSortings().size(); i++)
- {
- ui->sortByBox->addItem(filterModel->getAvailableSortings().keys().at(i));
- }
- ui->sortByBox->setCurrentText(filterModel->translateCurrentSorting());
-
- connect(ui->searchEdit, &QLineEdit::textChanged, this, &FtbPage::triggerSearch);
- connect(ui->sortByBox, &QComboBox::currentTextChanged, this, &FtbPage::onSortingSelectionChanged);
- connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FtbPage::onSelectionChanged);
- connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FtbPage::onVersionSelectionChanged);
-
- ui->packDescription->setMetaEntry("FTBPacks");
-}
-
-FtbPage::~FtbPage()
-{
- delete ui;
-}
-
-bool FtbPage::eventFilter(QObject* watched, QEvent* event)
-{
- if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) {
- QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event);
- if (keyEvent->key() == Qt::Key_Return) {
- triggerSearch();
- keyEvent->accept();
- return true;
- }
- }
- return QWidget::eventFilter(watched, event);
-}
-
-bool FtbPage::shouldDisplay() const
-{
- return true;
-}
-
-void FtbPage::retranslate()
-{
- ui->retranslateUi(this);
-}
-
-void FtbPage::openedImpl()
-{
- if(!initialised || listModel->wasAborted())
- {
- listModel->request();
- initialised = true;
- }
-
- suggestCurrent();
-}
-
-void FtbPage::closedImpl()
-{
- if (listModel->isMakingRequest())
- listModel->abortRequest();
-}
-
-void FtbPage::suggestCurrent()
-{
- if(!isOpened)
- {
- return;
- }
-
- if (selectedVersion.isEmpty())
- {
- dialog->setSuggestedPack();
- return;
- }
-
- dialog->setSuggestedPack(selected.name, selectedVersion, new ModpacksCH::PackInstallTask(selected, selectedVersion, this));
- for(auto art : selected.art) {
- if(art.type == "square") {
- QString editedLogoName;
- editedLogoName = selected.name;
-
- listModel->getLogo(selected.name, art.url, [this, editedLogoName](QString logo)
- {
- dialog->setSuggestedIconFromFile(logo + ".small", editedLogoName);
- });
- }
- }
-}
-
-void FtbPage::triggerSearch()
-{
- filterModel->setSearchTerm(ui->searchEdit->text());
-}
-
-void FtbPage::onSortingSelectionChanged(QString data)
-{
- auto toSet = filterModel->getAvailableSortings().value(data);
- filterModel->setSorting(toSet);
-}
-
-void FtbPage::onSelectionChanged(QModelIndex first, QModelIndex second)
-{
- ui->versionSelectionBox->clear();
-
- if(!first.isValid())
- {
- if(isOpened)
- {
- dialog->setSuggestedPack();
- }
- return;
- }
-
- selected = filterModel->data(first, Qt::UserRole).value<ModpacksCH::Modpack>();
-
- QString output = markdownToHTML(selected.description.toUtf8());
- ui->packDescription->setHtml(output);
-
- // reverse foreach, so that the newest versions are first
- for (auto i = selected.versions.size(); i--;) {
- ui->versionSelectionBox->addItem(selected.versions.at(i).name);
- }
-
- suggestCurrent();
-}
-
-void FtbPage::onVersionSelectionChanged(QString data)
-{
- if(data.isNull() || data.isEmpty())
- {
- selectedVersion = "";
- return;
- }
-
- selectedVersion = data;
- suggestCurrent();
-}
diff --git a/launcher/ui/pages/modplatform/ftb/FtbPage.h b/launcher/ui/pages/modplatform/ftb/FtbPage.h
deleted file mode 100644
index 631ae7f5..00000000
--- a/launcher/ui/pages/modplatform/ftb/FtbPage.h
+++ /dev/null
@@ -1,105 +0,0 @@
-// SPDX-License-Identifier: GPL-3.0-only
-/*
- * PolyMC - Minecraft Launcher
- * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
- *
- * 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 "FtbFilterModel.h"
-#include "FtbListModel.h"
-
-#include <QWidget>
-
-#include "Application.h"
-#include "ui/pages/BasePage.h"
-#include "tasks/Task.h"
-
-namespace Ui
-{
- class FtbPage;
-}
-
-class NewInstanceDialog;
-
-class FtbPage : public QWidget, public BasePage
-{
-Q_OBJECT
-
-public:
- explicit FtbPage(NewInstanceDialog* dialog, QWidget *parent = 0);
- virtual ~FtbPage();
- virtual QString displayName() const override
- {
- return "FTB";
- }
- virtual QIcon icon() const override
- {
- return APPLICATION->getThemedIcon("ftb_logo");
- }
- virtual QString id() const override
- {
- return "ftb";
- }
- virtual QString helpPage() const override
- {
- return "FTB-platform";
- }
- virtual bool shouldDisplay() const override;
- void retranslate() override;
-
- void openedImpl() override;
- void closedImpl() override;
-
- bool eventFilter(QObject * watched, QEvent * event) override;
-
-private:
- void suggestCurrent();
-
-private slots:
- void triggerSearch();
-
- void onSortingSelectionChanged(QString data);
- void onSelectionChanged(QModelIndex first, QModelIndex second);
- void onVersionSelectionChanged(QString data);
-
-private:
- Ui::FtbPage *ui = nullptr;
- NewInstanceDialog* dialog = nullptr;
- Ftb::ListModel* listModel = nullptr;
- Ftb::FilterModel* filterModel = nullptr;
-
- ModpacksCH::Modpack selected;
- QString selectedVersion;
-
- bool initialised { false };
-};
diff --git a/launcher/ui/pages/modplatform/ftb/FtbPage.ui b/launcher/ui/pages/modplatform/ftb/FtbPage.ui
deleted file mode 100644
index 8de0f4e6..00000000
--- a/launcher/ui/pages/modplatform/ftb/FtbPage.ui
+++ /dev/null
@@ -1,86 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<ui version="4.0">
- <class>FtbPage</class>
- <widget class="QWidget" name="FtbPage">
- <property name="geometry">
- <rect>
- <x>0</x>
- <y>0</y>
- <width>875</width>
- <height>745</height>
- </rect>
- </property>
- <layout class="QGridLayout" name="gridLayout">
- <item row="2" column="0" colspan="2">
- <layout class="QGridLayout" name="gridLayout_4" columnstretch="0,0,0" rowminimumheight="0" columnminimumwidth="0,0,0">
- <item row="0" column="2">
- <widget class="QComboBox" name="versionSelectionBox"/>
- </item>
- <item row="0" column="1">
- <widget class="QLabel" name="label">
- <property name="text">
- <string>Version selected:</string>
- </property>
- <property name="alignment">
- <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
- </property>
- </widget>
- </item>
- <item row="0" column="0">
- <widget class="QComboBox" name="sortByBox"/>
- </item>
- </layout>
- </item>
- <item row="0" column="0">
- <widget class="QLineEdit" name="searchEdit">
- <property name="placeholderText">
- <string>Search and filter...</string>
- </property>
- <property name="clearButtonEnabled">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- <item row="1" column="0" colspan="2">
- <layout class="QGridLayout" name="gridLayout_3">
- <item row="0" column="0">
- <widget class="QTreeView" name="packView">
- <property name="alternatingRowColors">
- <bool>true</bool>
- </property>
- <property name="iconSize">
- <size>
- <width>48</width>
- <height>48</height>
- </size>
- </property>
- </widget>
- </item>
- <item row="0" column="1">
- <widget class="ProjectDescriptionPage" name="packDescription">
- <property name="openExternalLinks">
- <bool>true</bool>
- </property>
- <property name="openLinks">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- </layout>
- </widget>
- <customwidgets>
- <customwidget>
- <class>ProjectDescriptionPage</class>
- <extends>QTextBrowser</extends>
- <header>ui/widgets/ProjectDescriptionPage.h</header>
- </customwidget>
- </customwidgets>
- <tabstops>
- <tabstop>searchEdit</tabstop>
- <tabstop>versionSelectionBox</tabstop>
- </tabstops>
- <resources/>
- <connections/>
-</ui>
diff --git a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp
index 2343b79f..330dd4fb 100644
--- a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp
+++ b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp
@@ -38,11 +38,11 @@
#include "net/HttpMetaCache.h"
#include "net/NetJob.h"
-#include "StringUtils.h"
#include <Version.h>
+#include "StringUtils.h"
-#include <QtMath>
#include <QLabel>
+#include <QtMath>
#include <RWStorage.h>
@@ -50,33 +50,33 @@
namespace LegacyFTB {
-FilterModel::FilterModel(QObject *parent) : QSortFilterProxyModel(parent)
+FilterModel::FilterModel(QObject* parent) : QSortFilterProxyModel(parent)
{
currentSorting = Sorting::ByGameVersion;
sortings.insert(tr("Sort by Name"), Sorting::ByName);
sortings.insert(tr("Sort by Game Version"), Sorting::ByGameVersion);
}
-bool FilterModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
+bool FilterModel::lessThan(const QModelIndex& left, const QModelIndex& right) const
{
Modpack leftPack = sourceModel()->data(left, Qt::UserRole).value<Modpack>();
Modpack rightPack = sourceModel()->data(right, Qt::UserRole).value<Modpack>();
- if(currentSorting == Sorting::ByGameVersion) {
+ if (currentSorting == Sorting::ByGameVersion) {
Version lv(leftPack.mcVersion);
Version rv(rightPack.mcVersion);
return lv < rv;
- } else if(currentSorting == Sorting::ByName) {
+ } else if (currentSorting == Sorting::ByName) {
return StringUtils::naturalCompare(leftPack.name, rightPack.name, Qt::CaseSensitive) >= 0;
}
- //UHM, some inavlid value set?!
+ // UHM, some inavlid value set?!
qWarning() << "Invalid sorting set!";
return true;
}
-bool FilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
+bool FilterModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const
{
return true;
}
@@ -102,18 +102,13 @@ FilterModel::Sorting FilterModel::getCurrentSorting()
return currentSorting;
}
-ListModel::ListModel(QObject *parent) : QAbstractListModel(parent)
-{
-}
+ListModel::ListModel(QObject* parent) : QAbstractListModel(parent) {}
-ListModel::~ListModel()
-{
-}
+ListModel::~ListModel() {}
QString ListModel::translatePackType(PackType type) const
{
- switch(type)
- {
+ switch (type) {
case PackType::Public:
return tr("Public Modpack");
case PackType::ThirdParty:
@@ -125,67 +120,51 @@ QString ListModel::translatePackType(PackType type) const
return QString();
}
-int ListModel::rowCount(const QModelIndex &parent) const
+int ListModel::rowCount(const QModelIndex& parent) const
{
return parent.isValid() ? 0 : modpacks.size();
}
-int ListModel::columnCount(const QModelIndex &parent) const
+int ListModel::columnCount(const QModelIndex& parent) const
{
return parent.isValid() ? 0 : 1;
}
-QVariant ListModel::data(const QModelIndex &index, int role) const
+QVariant ListModel::data(const QModelIndex& index, int role) const
{
int pos = index.row();
- if(pos >= modpacks.size() || pos < 0 || !index.isValid())
- {
+ if (pos >= modpacks.size() || pos < 0 || !index.isValid()) {
return QString("INVALID INDEX %1").arg(pos);
}
Modpack pack = modpacks.at(pos);
- if(role == Qt::DisplayRole)
- {
+ if (role == Qt::DisplayRole) {
return pack.name + "\n" + translatePackType(pack.type);
- }
- else if (role == Qt::ToolTipRole)
- {
- if(pack.description.length() > 100)
- {
- //some magic to prevent to long tooltips and replace html linebreaks
+ } else if (role == 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;
- }
- else if(role == Qt::DecorationRole)
- {
- if(m_logoMap.contains(pack.logo))
- {
+ } else if (role == Qt::DecorationRole) {
+ if (m_logoMap.contains(pack.logo)) {
return (m_logoMap.value(pack.logo));
}
QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder");
- ((ListModel *)this)->requestLogo(pack.logo);
+ ((ListModel*)this)->requestLogo(pack.logo);
return icon;
- }
- else if(role == Qt::ForegroundRole)
- {
- if(pack.broken)
- {
- //FIXME: Hardcoded color
+ } else if (role == Qt::ForegroundRole) {
+ if (pack.broken) {
+ // FIXME: Hardcoded color
return QColor(255, 0, 50);
- }
- else if(pack.bugged)
- {
- //FIXME: Hardcoded color
- //bugged pack, currently only indicates bugged xml
+ } else if (pack.bugged) {
+ // FIXME: Hardcoded color
+ // bugged pack, currently only indicates bugged xml
return QColor(244, 229, 66);
}
- }
- else if(role == Qt::UserRole)
- {
+ } else if (role == Qt::UserRole) {
QVariant v;
v.setValue(pack);
return v;
@@ -222,8 +201,7 @@ Modpack ListModel::at(int row)
void ListModel::remove(int row)
{
- if(row < 0 || row >= modpacks.size())
- {
+ if (row < 0 || row >= modpacks.size()) {
qWarning() << "Attempt to remove FTB modpacks with invalid row" << row;
return;
}
@@ -247,27 +225,25 @@ void ListModel::logoFailed(QString logo)
void ListModel::requestLogo(QString file)
{
- if(m_loadingLogos.contains(file) || m_failedLogos.contains(file))
- {
+ if (m_loadingLogos.contains(file) || m_failedLogos.contains(file)) {
return;
}
MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("FTBPacks", QString("logos/%1").arg(file.section(".", 0, 0)));
- NetJob *job = new NetJob(QString("FTB Icon Download for %1").arg(file), APPLICATION->network());
+ NetJob* job = new NetJob(QString("FTB Icon Download for %1").arg(file), APPLICATION->network());
job->addNetAction(Net::Download::makeCached(QUrl(QString(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/%1").arg(file)), entry));
auto fullPath = entry->getFullPath();
- QObject::connect(job, &NetJob::finished, this, [this, file, fullPath]
- {
+ QObject::connect(job, &NetJob::finished, this, [this, file, fullPath, job] {
+ job->deleteLater();
emit logoLoaded(file, QIcon(fullPath));
- if(waitingCallbacks.contains(file))
- {
+ if (waitingCallbacks.contains(file)) {
waitingCallbacks.value(file)(fullPath);
}
});
- QObject::connect(job, &NetJob::failed, this, [this, file]
- {
+ QObject::connect(job, &NetJob::failed, this, [this, file, job] {
+ job->deleteLater();
emit logoFailed(file);
});
@@ -276,21 +252,18 @@ void ListModel::requestLogo(QString file)
m_loadingLogos.append(file);
}
-void ListModel::getLogo(const QString &logo, LogoCallback callback)
+void ListModel::getLogo(const QString& logo, LogoCallback callback)
{
- if(m_logoMap.contains(logo))
- {
+ if (m_logoMap.contains(logo)) {
callback(APPLICATION->metacache()->resolveEntry("FTBPacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath());
- }
- else
- {
+ } else {
requestLogo(logo);
}
}
-Qt::ItemFlags ListModel::flags(const QModelIndex &index) const
+Qt::ItemFlags ListModel::flags(const QModelIndex& index) const
{
return QAbstractListModel::flags(index);
}
-}
+} // namespace LegacyFTB
diff --git a/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp b/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp
index 98ab8799..b3f6261f 100644
--- a/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp
+++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp
@@ -116,8 +116,8 @@ Page::Page(NewInstanceDialog* dialog, QWidget *parent)
connect(ui->thirdPartyPackList->selectionModel(), &QItemSelectionModel::currentChanged, this, &Page::onThirdPartyPackSelectionChanged);
connect(ui->privatePackList->selectionModel(), &QItemSelectionModel::currentChanged, this, &Page::onPrivatePackSelectionChanged);
- connect(ui->addPackBtn, &QPushButton::pressed, this, &Page::onAddPackClicked);
- connect(ui->removePackBtn, &QPushButton::pressed, this, &Page::onRemovePackClicked);
+ connect(ui->addPackBtn, &QPushButton::clicked, this, &Page::onAddPackClicked);
+ connect(ui->removePackBtn, &QPushButton::clicked, this, &Page::onRemovePackClicked);
connect(ui->tabWidget, &QTabWidget::currentChanged, this, &Page::onTabChanged);
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp
index 346a00b0..e0046d88 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp
@@ -106,6 +106,8 @@ auto ModpackListModel::data(const QModelIndex& index, int role) const -> QVarian
return pack.description;
case UserDataTypes::SELECTED:
return false;
+ case UserDataTypes::INSTALLED:
+ return false;
default:
break;
}
@@ -129,27 +131,27 @@ void ModpackListModel::performPaginatedSearch()
// TODO: Move to standalone API
auto netJob = makeShared<NetJob>("Modrinth::SearchModpack", APPLICATION->network());
auto searchAllUrl = QString(BuildConfig.MODRINTH_PROD_URL +
- "/search?"
- "offset=%1&"
- "limit=%2&"
- "query=%3&"
- "index=%4&"
- "facets=[[\"project_type:modpack\"]]")
+ "/search?"
+ "offset=%1&"
+ "limit=%2&"
+ "query=%3&"
+ "index=%4&"
+ "facets=[[\"project_type:modpack\"]]")
.arg(nextSearchOffset)
.arg(m_modpacks_per_page)
.arg(currentSearchTerm)
.arg(currentSort);
- netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchAllUrl), &m_all_response));
+ netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchAllUrl), m_all_response));
QObject::connect(netJob.get(), &NetJob::succeeded, this, [this] {
QJsonParseError parse_error_all{};
- QJsonDocument doc_all = QJsonDocument::fromJson(m_all_response, &parse_error_all);
+ QJsonDocument doc_all = QJsonDocument::fromJson(*m_all_response, &parse_error_all);
if (parse_error_all.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from " << debugName() << " at " << parse_error_all.offset
<< " reason: " << parse_error_all.errorString();
- qWarning() << m_all_response;
+ qWarning() << *m_all_response;
return;
}
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h
index 6e6be4b9..b9e9c3da 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h
@@ -110,9 +110,9 @@ class ModpackListModel : public QAbstractListModel {
NetJob::Ptr jobPtr;
- QByteArray m_all_response;
+ std::shared_ptr<QByteArray> m_all_response = std::make_shared<QByteArray>();
QByteArray m_specific_response;
int m_modpacks_per_page = 20;
};
-} // namespace ModPlatform
+} // namespace Modrinth
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp
index 0bb11d83..c71dd903 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp
@@ -123,7 +123,7 @@ void ModrinthPage::onSelectionChanged(QModelIndex curr, QModelIndex prev)
qDebug() << "Loading modrinth modpack information";
auto netJob = new NetJob(QString("Modrinth::PackInformation(%1)").arg(current.name), APPLICATION->network());
- auto response = new QByteArray();
+ auto response = std::make_shared<QByteArray>();
QString id = current.id;
@@ -162,10 +162,7 @@ void ModrinthPage::onSelectionChanged(QModelIndex curr, QModelIndex prev)
suggestCurrent();
});
- QObject::connect(netJob, &NetJob::finished, this, [response, netJob] {
- netJob->deleteLater();
- delete response;
- });
+ QObject::connect(netJob, &NetJob::finished, this, [response, netJob] { netJob->deleteLater(); });
netJob->start();
} else
updateUI();
@@ -174,7 +171,7 @@ void ModrinthPage::onSelectionChanged(QModelIndex curr, QModelIndex prev)
qDebug() << "Loading modrinth modpack versions";
auto netJob = new NetJob(QString("Modrinth::PackVersions(%1)").arg(current.name), APPLICATION->network());
- auto response = new QByteArray();
+ auto response = std::make_shared<QByteArray>();
QString id = current.id;
@@ -217,10 +214,7 @@ void ModrinthPage::onSelectionChanged(QModelIndex curr, QModelIndex prev)
suggestCurrent();
});
- QObject::connect(netJob, &NetJob::finished, this, [response, netJob] {
- netJob->deleteLater();
- delete response;
- });
+ QObject::connect(netJob, &NetJob::finished, this, [response, netJob] { netJob->deleteLater(); });
netJob->start();
} else {
@@ -260,10 +254,8 @@ void ModrinthPage::updateUI()
text += donates.join(", ");
}
- if (!current.extra.issuesUrl.isEmpty()
- || !current.extra.sourceUrl.isEmpty()
- || !current.extra.wikiUrl.isEmpty()
- || !current.extra.discordUrl.isEmpty()) {
+ if (!current.extra.issuesUrl.isEmpty() || !current.extra.sourceUrl.isEmpty() || !current.extra.wikiUrl.isEmpty() ||
+ !current.extra.discordUrl.isEmpty()) {
text += "<br><br>" + tr("External links:") + "<br>";
}
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp
index f5d1cc28..8aa64989 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp
@@ -25,7 +25,7 @@
namespace ResourceDownload {
-ModrinthModModel::ModrinthModModel(BaseInstance const& base) : ModModel(base, new ModrinthAPI) {}
+ModrinthModModel::ModrinthModModel(BaseInstance& base) : ModModel(base, new ModrinthAPI) {}
void ModrinthModModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj)
{
@@ -42,12 +42,17 @@ void ModrinthModModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJso
::Modrinth::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance);
}
+auto ModrinthModModel::loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion
+{
+ return ::Modrinth::loadDependencyVersions(m, arr);
+};
+
auto ModrinthModModel::documentToArray(QJsonDocument& obj) const -> QJsonArray
{
return obj.object().value("hits").toArray();
}
-ModrinthResourcePackModel::ModrinthResourcePackModel(const BaseInstance& base) : ResourcePackResourceModel(base, new ModrinthAPI){}
+ModrinthResourcePackModel::ModrinthResourcePackModel(const BaseInstance& base) : ResourcePackResourceModel(base, new ModrinthAPI) {}
void ModrinthResourcePackModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj)
{
@@ -69,7 +74,7 @@ auto ModrinthResourcePackModel::documentToArray(QJsonDocument& obj) const -> QJs
return obj.object().value("hits").toArray();
}
-ModrinthTexturePackModel::ModrinthTexturePackModel(const BaseInstance& base) : TexturePackResourceModel(base, new ModrinthAPI){}
+ModrinthTexturePackModel::ModrinthTexturePackModel(const BaseInstance& base) : TexturePackResourceModel(base, new ModrinthAPI) {}
void ModrinthTexturePackModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj)
{
@@ -91,7 +96,7 @@ auto ModrinthTexturePackModel::documentToArray(QJsonDocument& obj) const -> QJso
return obj.object().value("hits").toArray();
}
-ModrinthShaderPackModel::ModrinthShaderPackModel(const BaseInstance& base) : ShaderPackResourceModel(base, new ModrinthAPI){}
+ModrinthShaderPackModel::ModrinthShaderPackModel(const BaseInstance& base) : ShaderPackResourceModel(base, new ModrinthAPI) {}
void ModrinthShaderPackModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj)
{
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h
index b351b19b..d7c858f8 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h
@@ -30,7 +30,7 @@ class ModrinthModModel : public ModModel {
Q_OBJECT
public:
- ModrinthModModel(const BaseInstance&);
+ ModrinthModModel(BaseInstance&);
~ModrinthModModel() override = default;
private:
@@ -40,6 +40,7 @@ class ModrinthModModel : public ModModel {
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 loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion override;
auto documentToArray(QJsonDocument& obj) const -> QJsonArray override;
};
diff --git a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp
index 50f0c72d..f08eb289 100644
--- a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp
+++ b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp
@@ -40,39 +40,28 @@
#include <QIcon>
-Technic::ListModel::ListModel(QObject *parent) : QAbstractListModel(parent)
-{
-}
+Technic::ListModel::ListModel(QObject* parent) : QAbstractListModel(parent) {}
-Technic::ListModel::~ListModel()
-{
-}
+Technic::ListModel::~ListModel() {}
QVariant Technic::ListModel::data(const QModelIndex& index, int role) const
{
int pos = index.row();
- if(pos >= modpacks.size() || pos < 0 || !index.isValid())
- {
+ if (pos >= modpacks.size() || pos < 0 || !index.isValid()) {
return QString("INVALID INDEX %1").arg(pos);
}
Modpack pack = modpacks.at(pos);
- if(role == Qt::DisplayRole)
- {
+ if (role == Qt::DisplayRole) {
return pack.name;
- }
- else if(role == Qt::DecorationRole)
- {
- if(m_logoMap.contains(pack.logoName))
- {
+ } else if (role == Qt::DecorationRole) {
+ if (m_logoMap.contains(pack.logoName)) {
return (m_logoMap.value(pack.logoName));
}
QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder");
- ((ListModel *)this)->requestLogo(pack.logoName, pack.logoUrl);
+ ((ListModel*)this)->requestLogo(pack.logoName, pack.logoUrl);
return icon;
- }
- else if(role == Qt::UserRole)
- {
+ } else if (role == Qt::UserRole) {
QVariant v;
v.setValue(pack);
return v;
@@ -92,16 +81,15 @@ int Technic::ListModel::rowCount(const QModelIndex& parent) const
void Technic::ListModel::searchWithTerm(const QString& term)
{
- if(currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull()) {
+ if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull()) {
return;
}
currentSearchTerm = term;
- if(jobPtr) {
+ if (jobPtr) {
jobPtr->abort();
searchState = ResetRequested;
return;
- }
- else {
+ } else {
beginResetModel();
modpacks.clear();
endResetModel();
@@ -115,26 +103,20 @@ void Technic::ListModel::performSearch()
auto netJob = makeShared<NetJob>("Technic::Search", APPLICATION->network());
QString searchUrl = "";
if (currentSearchTerm.isEmpty()) {
- searchUrl = QString("%1trending?build=%2")
- .arg(BuildConfig.TECHNIC_API_BASE_URL, BuildConfig.TECHNIC_API_BUILD);
+ searchUrl = QString("%1trending?build=%2").arg(BuildConfig.TECHNIC_API_BASE_URL, BuildConfig.TECHNIC_API_BUILD);
searchMode = List;
- }
- else if (currentSearchTerm.startsWith("http://api.technicpack.net/modpack/")) {
- searchUrl = QString("https://%1?build=%2")
- .arg(currentSearchTerm.mid(7), BuildConfig.TECHNIC_API_BUILD);
+ } else if (currentSearchTerm.startsWith("http://api.technicpack.net/modpack/")) {
+ searchUrl = QString("https://%1?build=%2").arg(currentSearchTerm.mid(7), BuildConfig.TECHNIC_API_BUILD);
searchMode = Single;
- }
- else if (currentSearchTerm.startsWith("https://api.technicpack.net/modpack/")) {
+ } else if (currentSearchTerm.startsWith("https://api.technicpack.net/modpack/")) {
searchUrl = QString("%1?build=%2").arg(currentSearchTerm, BuildConfig.TECHNIC_API_BUILD);
searchMode = Single;
- }
- else {
- searchUrl = QString(
- "%1search?build=%2&q=%3"
- ).arg(BuildConfig.TECHNIC_API_BASE_URL, BuildConfig.TECHNIC_API_BUILD, currentSearchTerm);
+ } else {
+ searchUrl =
+ QString("%1search?build=%2&q=%3").arg(BuildConfig.TECHNIC_API_BASE_URL, BuildConfig.TECHNIC_API_BUILD, currentSearchTerm);
searchMode = List;
}
- netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response));
+ netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), response));
jobPtr = netJob;
jobPtr->start();
QObject::connect(netJob.get(), &NetJob::succeeded, this, &ListModel::searchRequestFinished);
@@ -146,11 +128,11 @@ void Technic::ListModel::searchRequestFinished()
jobPtr.reset();
QJsonParseError parse_error;
- QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error);
- if(parse_error.error != QJsonParseError::NoError)
- {
- qWarning() << "Error while parsing JSON response from Technic at " << parse_error.offset << " reason: " << parse_error.errorString();
- qWarning() << response;
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
+ if (parse_error.error != QJsonParseError::NoError) {
+ qWarning() << "Error while parsing JSON response from Technic at " << parse_error.offset
+ << " reason: " << parse_error.errorString();
+ qWarning() << *response;
return;
}
@@ -161,7 +143,7 @@ void Technic::ListModel::searchRequestFinished()
switch (searchMode) {
case List: {
auto objs = Json::requireArray(root, "modpacks");
- for (auto technicPack: objs) {
+ for (auto technicPack : objs) {
Modpack pack;
auto technicPackObject = Json::requireObject(technicPack);
pack.name = Json::requireString(technicPackObject, "name");
@@ -170,11 +152,10 @@ void Technic::ListModel::searchRequestFinished()
continue;
auto rawURL = Json::ensureString(technicPackObject, "iconUrl", "null");
- if(rawURL == "null") {
+ if (rawURL == "null") {
pack.logoUrl = "null";
pack.logoName = "null";
- }
- else {
+ } else {
pack.logoUrl = rawURL;
pack.logoName = rawURL.section(QLatin1Char('/'), -1).section(QLatin1Char('.'), 0, 0);
}
@@ -199,8 +180,7 @@ void Technic::ListModel::searchRequestFinished()
pack.logoUrl = iconUrl;
pack.logoName = iconUrl.section(QLatin1Char('/'), -1).section(QLatin1Char('.'), 0, 0);
- }
- else {
+ } else {
pack.logoUrl = "null";
pack.logoName = "null";
}
@@ -210,10 +190,8 @@ void Technic::ListModel::searchRequestFinished()
break;
}
}
- }
- catch (const JSONValidationError &err)
- {
- qCritical() << "Couldn't parse technic search results:" << err.cause() ;
+ } catch (const JSONValidationError& err) {
+ qCritical() << "Couldn't parse technic search results:" << err.cause();
return;
}
searchState = Finished;
@@ -229,12 +207,9 @@ void Technic::ListModel::searchRequestFinished()
void Technic::ListModel::getLogo(const QString& logo, const QString& logoUrl, Technic::LogoCallback callback)
{
- if(m_logoMap.contains(logo))
- {
+ if (m_logoMap.contains(logo)) {
callback(APPLICATION->metacache()->resolveEntry("TechnicPacks", QString("logos/%1").arg(logo))->getFullPath());
- }
- else
- {
+ } else {
requestLogo(logo, logoUrl);
}
}
@@ -243,30 +218,24 @@ void Technic::ListModel::searchRequestFailed()
{
jobPtr.reset();
- if(searchState == ResetRequested)
- {
+ if (searchState == ResetRequested) {
beginResetModel();
modpacks.clear();
endResetModel();
performSearch();
- }
- else
- {
+ } else {
searchState = Finished;
}
}
-
void Technic::ListModel::logoLoaded(QString logo, QString out)
{
m_loadingLogos.removeAll(logo);
m_logoMap.insert(logo, QIcon(out));
- for(int i = 0; i < modpacks.size(); i++)
- {
- if(modpacks[i].logoName == logo)
- {
- emit dataChanged(createIndex(i, 0), createIndex(i, 0), {Qt::DecorationRole});
+ for (int i = 0; i < modpacks.size(); i++) {
+ if (modpacks[i].logoName == logo) {
+ emit dataChanged(createIndex(i, 0), createIndex(i, 0), { Qt::DecorationRole });
}
}
}
@@ -279,24 +248,23 @@ void Technic::ListModel::logoFailed(QString logo)
void Technic::ListModel::requestLogo(QString logo, QString url)
{
- if(m_loadingLogos.contains(logo) || m_failedLogos.contains(logo) || logo == "null")
- {
+ if (m_loadingLogos.contains(logo) || m_failedLogos.contains(logo) || logo == "null") {
return;
}
MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("TechnicPacks", QString("logos/%1").arg(logo));
- NetJob *job = new NetJob(QString("Technic Icon Download %1").arg(logo), APPLICATION->network());
+ auto job = new NetJob(QString("Technic Icon Download %1").arg(logo), APPLICATION->network());
job->addNetAction(Net::Download::makeCached(QUrl(url), entry));
auto fullPath = entry->getFullPath();
- QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath]
- {
+ QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] {
+ job->deleteLater();
logoLoaded(logo, fullPath);
});
- QObject::connect(job, &NetJob::failed, this, [this, logo]
- {
+ QObject::connect(job, &NetJob::failed, this, [this, logo, job] {
+ job->deleteLater();
logoFailed(logo);
});
diff --git a/launcher/ui/pages/modplatform/technic/TechnicModel.h b/launcher/ui/pages/modplatform/technic/TechnicModel.h
index 5eea124c..0f1a814e 100644
--- a/launcher/ui/pages/modplatform/technic/TechnicModel.h
+++ b/launcher/ui/pages/modplatform/technic/TechnicModel.h
@@ -44,33 +44,32 @@ namespace Technic {
typedef std::function<void(QString)> LogoCallback;
-class ListModel : public QAbstractListModel
-{
+class ListModel : public QAbstractListModel {
Q_OBJECT
-public:
- ListModel(QObject *parent);
+ public:
+ ListModel(QObject* parent);
virtual ~ListModel();
virtual QVariant data(const QModelIndex& index, int role) const;
virtual int columnCount(const QModelIndex& parent) const;
virtual int rowCount(const QModelIndex& parent) const;
- void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback);
- void searchWithTerm(const QString & term);
+ void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback);
+ void searchWithTerm(const QString& term);
-private slots:
+ private slots:
void searchRequestFinished();
void searchRequestFailed();
void logoFailed(QString logo);
void logoLoaded(QString logo, QString out);
-private:
+ private:
void performSearch();
void requestLogo(QString logo, QString url);
-private:
+ private:
QList<Modpack> modpacks;
QStringList m_failedLogos;
QStringList m_loadingLogos;
@@ -78,17 +77,13 @@ private:
QMap<QString, LogoCallback> waitingCallbacks;
QString currentSearchTerm;
- enum SearchState {
- None,
- ResetRequested,
- Finished
- } searchState = None;
+ enum SearchState { None, ResetRequested, Finished } searchState = None;
enum SearchMode {
List,
Single,
} searchMode = List;
NetJob::Ptr jobPtr;
- QByteArray response;
+ std::shared_ptr<QByteArray> response = std::make_shared<QByteArray>();
};
-}
+} // namespace Technic
diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp
index 859da97e..fc678fa2 100644
--- a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp
+++ b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp
@@ -143,7 +143,7 @@ void TechnicPage::suggestCurrent()
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));
+ 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.get(), &NetJob::succeeded, this, [this, slug]
{
jobPtr.reset();
@@ -154,7 +154,7 @@ void TechnicPage::suggestCurrent()
}
QJsonParseError parse_error {};
- QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error);
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
QJsonObject obj = doc.object();
if(parse_error.error != QJsonParseError::NoError)
{
@@ -249,7 +249,7 @@ void TechnicPage::metadataLoaded()
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));
+ netJob->addNetAction(Net::Download::makeByteArray(QUrl(url), response));
QObject::connect(netJob.get(), &NetJob::succeeded, this, &TechnicPage::onSolderLoaded);
@@ -291,11 +291,11 @@ void TechnicPage::onSolderLoaded() {
current.versions.clear();
- QJsonParseError parse_error {};
- auto doc = QJsonDocument::fromJson(response, &parse_error);
+ QJsonParseError parse_error{};
+ auto doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from Solder at " << parse_error.offset << " reason: " << parse_error.errorString();
- qWarning() << response;
+ qWarning() << *response;
fallback();
return;
}
@@ -304,8 +304,7 @@ void TechnicPage::onSolderLoaded() {
TechnicSolder::Pack pack;
try {
TechnicSolder::loadPack(pack, obj);
- }
- catch (const JSONValidationError& err) {
+ } catch (const JSONValidationError& err) {
qCritical() << "Couldn't parse Solder pack metadata:" << err.cause();
fallback();
return;
diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.h b/launcher/ui/pages/modplatform/technic/TechnicPage.h
index f4a3b61d..753261b3 100644
--- a/launcher/ui/pages/modplatform/technic/TechnicPage.h
+++ b/launcher/ui/pages/modplatform/technic/TechnicPage.h
@@ -104,5 +104,5 @@ private:
QString selectedVersion;
NetJob::Ptr jobPtr;
- QByteArray response;
+ std::shared_ptr<QByteArray> response = std::make_shared<QByteArray>();
};
diff --git a/launcher/ui/setupwizard/JavaWizardPage.cpp b/launcher/ui/setupwizard/JavaWizardPage.cpp
index 14683778..2b70c47c 100644
--- a/launcher/ui/setupwizard/JavaWizardPage.cpp
+++ b/launcher/ui/setupwizard/JavaWizardPage.cpp
@@ -69,6 +69,7 @@ bool JavaWizardPage::validatePage()
case JavaSettingsWidget::ValidationStatus::AllOK:
{
settings->set("JavaPath", m_java_widget->javaPath());
+ return true;
}
case JavaSettingsWidget::ValidationStatus::JavaBad:
{
diff --git a/launcher/ui/setupwizard/SetupWizard.cpp b/launcher/ui/setupwizard/SetupWizard.cpp
index 3fd9bb23..0a47334f 100644
--- a/launcher/ui/setupwizard/SetupWizard.cpp
+++ b/launcher/ui/setupwizard/SetupWizard.cpp
@@ -59,7 +59,7 @@ void SetupWizard::pageChanged(int id)
{
setButtonLayout({QWizard::CustomButton1, QWizard::Stretch, QWizard::BackButton, QWizard::NextButton, QWizard::FinishButton});
auto customButton = button(QWizard::CustomButton1);
- connect(customButton, &QAbstractButton::pressed, [&](){
+ connect(customButton, &QAbstractButton::clicked, [&](){
auto basePagePtr = getCurrentBasePage();
if(basePagePtr)
{
diff --git a/launcher/ui/themes/SystemTheme.cpp b/launcher/ui/themes/SystemTheme.cpp
index a95bc875..3a746d02 100644
--- a/launcher/ui/themes/SystemTheme.cpp
+++ b/launcher/ui/themes/SystemTheme.cpp
@@ -43,7 +43,7 @@ SystemTheme::SystemTheme()
{
themeDebugLog() << "Determining System Theme...";
const auto& style = QApplication::style();
- systemPalette = style->standardPalette();
+ systemPalette = QApplication::palette();
QString lowerThemeName = style->objectName();
themeDebugLog() << "System theme seems to be:" << lowerThemeName;
QStringList styles = QStyleFactory::keys();
diff --git a/launcher/ui/widgets/InfoFrame.cpp b/launcher/ui/widgets/InfoFrame.cpp
index fdc581b4..a0fda952 100644
--- a/launcher/ui/widgets/InfoFrame.cpp
+++ b/launcher/ui/widgets/InfoFrame.cpp
@@ -1,52 +1,70 @@
// 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/>.
-*
-* 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.
-*/
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
+ * Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * 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 <QLabel>
#include <QMessageBox>
+#include <QToolTip>
#include "InfoFrame.h"
#include "ui_InfoFrame.h"
#include "ui/dialogs/CustomMessageBox.h"
-InfoFrame::InfoFrame(QWidget *parent) :
- QFrame(parent),
- ui(new Ui::InfoFrame)
+void setupLinkToolTip(QLabel* label)
+{
+ QObject::connect(label, &QLabel::linkHovered, [label](const QString& link) {
+ if (auto url = QUrl(link); !url.isValid() || (url.scheme() != "http" && url.scheme() != "https"))
+ return;
+ label->setToolTip(link);
+ });
+}
+
+InfoFrame::InfoFrame(QWidget* parent) : QFrame(parent), ui(new Ui::InfoFrame)
{
ui->setupUi(this);
ui->descriptionLabel->setHidden(true);
ui->nameLabel->setHidden(true);
+ ui->licenseLabel->setHidden(true);
+ ui->issueTrackerLabel->setHidden(true);
+
+ setupLinkToolTip(ui->iconLabel);
+ setupLinkToolTip(ui->descriptionLabel);
+ setupLinkToolTip(ui->nameLabel);
+ setupLinkToolTip(ui->licenseLabel);
+ setupLinkToolTip(ui->issueTrackerLabel);
updateHiddenState();
}
@@ -57,38 +75,70 @@ InfoFrame::~InfoFrame()
void InfoFrame::updateWithMod(Mod const& m)
{
- if (m.type() == ResourceType::FOLDER)
- {
+ if (m.type() == ResourceType::FOLDER) {
clear();
return;
}
QString text = "";
QString name = "";
+ QString link = m.metaurl();
if (m.name().isEmpty())
name = m.internal_id();
else
name = m.name();
- if (m.homeurl().isEmpty())
+ if (link.isEmpty())
text = name;
- else
- text = "<a href=\"" + m.homeurl() + "\">" + name + "</a>";
+ else {
+ text = "<a href=\"" + link + "\">" + name + "</a>";
+ }
if (!m.authors().isEmpty())
text += " by " + m.authors().join(", ");
setName(text);
- if (m.description().isEmpty())
- {
+ if (m.description().isEmpty()) {
setDescription(QString());
- }
- else
- {
+ } else {
setDescription(m.description());
}
- setImage();
+ setImage(m.icon({ 64, 64 }));
+
+ auto licenses = m.licenses();
+ QString licenseText = "";
+ if (!licenses.empty()) {
+ for (auto l : licenses) {
+ if (!licenseText.isEmpty()) {
+ licenseText += "\n"; // add newline between licenses
+ }
+ if (!l.name.isEmpty()) {
+ if (l.url.isEmpty()) {
+ licenseText += l.name;
+ } else {
+ licenseText += "<a href=\"" + l.url + "\">" + l.name + "</a>";
+ }
+ } else if (!l.url.isEmpty()) {
+ licenseText += "<a href=\"" + l.url + "\">" + l.url + "</a>";
+ }
+ if (!l.description.isEmpty() && l.description != l.name) {
+ licenseText += " " + l.description;
+ }
+ }
+ }
+ if (!licenseText.isEmpty()) {
+ setLicense(tr("License: %1").arg(licenseText));
+ } else {
+ setLicense();
+ }
+
+ QString issueTracker = "";
+ if (!m.issueTracker().isEmpty()) {
+ issueTracker += tr("Report issues to: ");
+ issueTracker += "<a href=\"" + m.issueTracker() + "\">" + m.issueTracker() + "</a>";
+ }
+ setIssueTracker(issueTracker);
}
void InfoFrame::updateWithResource(const Resource& resource)
@@ -97,7 +147,8 @@ void InfoFrame::updateWithResource(const Resource& resource)
setImage();
}
-QString InfoFrame::renderColorCodes(QString input) {
+QString InfoFrame::renderColorCodes(QString input)
+{
// We have to manually set the colors for use.
//
// A color is set using §x, with x = a hex number from 0 to f.
@@ -108,16 +159,12 @@ QString InfoFrame::renderColorCodes(QString input) {
// TODO: Wrap links inside <a> tags
// https://minecraft.fandom.com/wiki/Formatting_codes#Color_codes
- const QMap<QChar, QString> color_codes_map = {
- {'0', "#000000"}, {'1', "#0000AA"}, {'2', "#00AA00"}, {'3', "#00AAAA"}, {'4', "#AA0000"},
- {'5', "#AA00AA"}, {'6', "#FFAA00"}, {'7', "#AAAAAA"}, {'8', "#555555"}, {'9', "#5555FF"},
- {'a', "#55FF55"}, {'b', "#55FFFF"}, {'c', "#FF5555"}, {'d', "#FF55FF"}, {'e', "#FFFF55"},
- {'f', "#FFFFFF"}
- };
+ const QMap<QChar, QString> color_codes_map = { { '0', "#000000" }, { '1', "#0000AA" }, { '2', "#00AA00" }, { '3', "#00AAAA" },
+ { '4', "#AA0000" }, { '5', "#AA00AA" }, { '6', "#FFAA00" }, { '7', "#AAAAAA" },
+ { '8', "#555555" }, { '9', "#5555FF" }, { 'a', "#55FF55" }, { 'b', "#55FFFF" },
+ { 'c', "#FF5555" }, { 'd', "#FF55FF" }, { 'e', "#FFFF55" }, { 'f', "#FFFFFF" } };
// https://minecraft.fandom.com/wiki/Formatting_codes#Formatting_codes
- const QMap<QChar, QString> formatting_codes_map = {
- {'l', "b"}, {'m', "s"}, {'n', "u"}, {'o', "i"}
- };
+ const QMap<QChar, QString> formatting_codes_map = { { 'l', "b" }, { 'm', "s" }, { 'n', "u" }, { 'o', "i" } };
QString html("<html>");
QList<QString> tags{};
@@ -162,14 +209,14 @@ void InfoFrame::updateWithResourcePack(ResourcePack& resource_pack)
{
setName(renderColorCodes(resource_pack.name()));
setDescription(renderColorCodes(resource_pack.description()));
- setImage(resource_pack.image({64, 64}));
+ setImage(resource_pack.image({ 64, 64 }));
}
void InfoFrame::updateWithTexturePack(TexturePack& texture_pack)
{
setName(renderColorCodes(texture_pack.name()));
setDescription(renderColorCodes(texture_pack.description()));
- setImage(texture_pack.image({64, 64}));
+ setImage(texture_pack.image({ 64, 64 }));
}
void InfoFrame::clear()
@@ -177,28 +224,25 @@ void InfoFrame::clear()
setName();
setDescription();
setImage();
+ setLicense();
+ setIssueTracker();
}
void InfoFrame::updateHiddenState()
{
- if(ui->descriptionLabel->isHidden() && ui->nameLabel->isHidden())
- {
+ if (ui->descriptionLabel->isHidden() && ui->nameLabel->isHidden() && ui->licenseLabel->isHidden() &&
+ ui->issueTrackerLabel->isHidden()) {
setHidden(true);
- }
- else
- {
+ } else {
setHidden(false);
}
}
void InfoFrame::setName(QString text)
{
- if(text.isEmpty())
- {
+ if (text.isEmpty()) {
ui->nameLabel->setHidden(true);
- }
- else
- {
+ } else {
ui->nameLabel->setText(text);
ui->nameLabel->setHidden(false);
}
@@ -207,14 +251,11 @@ void InfoFrame::setName(QString text)
void InfoFrame::setDescription(QString text)
{
- if(text.isEmpty())
- {
+ if (text.isEmpty()) {
ui->descriptionLabel->setHidden(true);
updateHiddenState();
return;
- }
- else
- {
+ } else {
ui->descriptionLabel->setHidden(false);
updateHiddenState();
}
@@ -224,9 +265,8 @@ void InfoFrame::setDescription(QString text)
QChar rem('\n');
QString finaltext;
finaltext.reserve(intermediatetext.size());
- foreach(const QChar& c, intermediatetext)
- {
- if(c == rem && prev){
+ foreach (const QChar& c, intermediatetext) {
+ if (c == rem && prev) {
continue;
}
prev = c == rem;
@@ -234,23 +274,70 @@ void InfoFrame::setDescription(QString text)
}
QString labeltext;
labeltext.reserve(300);
- if(finaltext.length() > 290)
- {
+ if (finaltext.length() > 290) {
ui->descriptionLabel->setOpenExternalLinks(false);
ui->descriptionLabel->setTextFormat(Qt::TextFormat::RichText);
m_description = text;
// This allows injecting HTML here.
labeltext.append("<html><body>" + finaltext.left(287) + "<a href=\"#mod_desc\">...</a></body></html>");
QObject::connect(ui->descriptionLabel, &QLabel::linkActivated, this, &InfoFrame::descriptionEllipsisHandler);
- }
- else
- {
+ } else {
ui->descriptionLabel->setTextFormat(Qt::TextFormat::AutoText);
labeltext.append(finaltext);
}
ui->descriptionLabel->setText(labeltext);
}
+void InfoFrame::setLicense(QString text)
+{
+ if (text.isEmpty()) {
+ ui->licenseLabel->setHidden(true);
+ updateHiddenState();
+ return;
+ } else {
+ ui->licenseLabel->setHidden(false);
+ updateHiddenState();
+ }
+ ui->licenseLabel->setToolTip("");
+ QString intermediatetext = text.trimmed();
+ bool prev(false);
+ QChar rem('\n');
+ QString finaltext;
+ finaltext.reserve(intermediatetext.size());
+ foreach (const QChar& c, intermediatetext) {
+ if (c == rem && prev) {
+ continue;
+ }
+ prev = c == rem;
+ finaltext += c;
+ }
+ QString labeltext;
+ labeltext.reserve(300);
+ if (finaltext.length() > 290) {
+ ui->licenseLabel->setOpenExternalLinks(false);
+ ui->licenseLabel->setTextFormat(Qt::TextFormat::RichText);
+ m_description = text;
+ // This allows injecting HTML here.
+ labeltext.append("<html><body>" + finaltext.left(287) + "<a href=\"#mod_desc\">...</a></body></html>");
+ QObject::connect(ui->licenseLabel, &QLabel::linkActivated, this, &InfoFrame::licenseEllipsisHandler);
+ } else {
+ ui->licenseLabel->setTextFormat(Qt::TextFormat::AutoText);
+ labeltext.append(finaltext);
+ }
+ ui->licenseLabel->setText(labeltext);
+}
+
+void InfoFrame::setIssueTracker(QString text)
+{
+ if (text.isEmpty()) {
+ ui->issueTrackerLabel->setHidden(true);
+ } else {
+ ui->issueTrackerLabel->setText(text);
+ ui->issueTrackerLabel->setHidden(false);
+ }
+ updateHiddenState();
+}
+
void InfoFrame::setImage(QPixmap img)
{
if (img.isNull()) {
@@ -263,18 +350,26 @@ void InfoFrame::setImage(QPixmap img)
void InfoFrame::descriptionEllipsisHandler(QString link)
{
- if(!m_current_box)
- {
+ if (!m_current_box) {
m_current_box = CustomMessageBox::selectable(this, "", m_description);
connect(m_current_box, &QMessageBox::finished, this, &InfoFrame::boxClosed);
m_current_box->show();
- }
- else
- {
+ } else {
m_current_box->setText(m_description);
}
}
+void InfoFrame::licenseEllipsisHandler(QString link)
+{
+ if (!m_current_box) {
+ m_current_box = CustomMessageBox::selectable(this, "", m_license);
+ connect(m_current_box, &QMessageBox::finished, this, &InfoFrame::boxClosed);
+ m_current_box->show();
+ } else {
+ m_current_box->setText(m_license);
+ }
+}
+
void InfoFrame::boxClosed(int result)
{
m_current_box = nullptr;
diff --git a/launcher/ui/widgets/InfoFrame.h b/launcher/ui/widgets/InfoFrame.h
index 84523e28..d6764baa 100644
--- a/launcher/ui/widgets/InfoFrame.h
+++ b/launcher/ui/widgets/InfoFrame.h
@@ -1,16 +1,36 @@
-/* Copyright 2013-2021 MultiMC Contributors
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
+ * 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.
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
*
- * Unless required by applicable law or agreed to in writing, software
- * 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.
+ * 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
@@ -21,8 +41,7 @@
#include "minecraft/mod/ResourcePack.h"
#include "minecraft/mod/TexturePack.h"
-namespace Ui
-{
+namespace Ui {
class InfoFrame;
}
@@ -36,6 +55,8 @@ class InfoFrame : public QFrame {
void setName(QString text = {});
void setDescription(QString text = {});
void setImage(QPixmap img = {});
+ void setLicense(QString text = {});
+ void setIssueTracker(QString text = {});
void clear();
@@ -48,6 +69,7 @@ class InfoFrame : public QFrame {
public slots:
void descriptionEllipsisHandler(QString link);
+ void licenseEllipsisHandler(QString link);
void boxClosed(int result);
private:
@@ -56,5 +78,6 @@ class InfoFrame : public QFrame {
private:
Ui::InfoFrame* ui;
QString m_description;
+ QString m_license;
class QMessageBox* m_current_box = nullptr;
};
diff --git a/launcher/ui/widgets/InfoFrame.ui b/launcher/ui/widgets/InfoFrame.ui
index 9e407ce9..c4d8c83d 100644
--- a/launcher/ui/widgets/InfoFrame.ui
+++ b/launcher/ui/widgets/InfoFrame.ui
@@ -35,8 +35,36 @@
<property name="bottomMargin">
<number>0</number>
</property>
- <item row="0" column="1">
- <widget class="QLabel" name="nameLabel">
+ <item row="0" column="0" rowspan="2">
+ <widget class="QLabel" name="iconLabel">
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>64</width>
+ <height>64</height>
+ </size>
+ </property>
+ <property name="text">
+ <string notr="true"/>
+ </property>
+ <property name="scaledContents">
+ <bool>false</bool>
+ </property>
+ <property name="margin">
+ <number>0</number>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QLabel" name="descriptionLabel">
+ <property name="toolTip">
+ <string notr="true"/>
+ </property>
<property name="text">
<string notr="true"/>
</property>
@@ -57,11 +85,8 @@
</property>
</widget>
</item>
- <item row="1" column="1">
- <widget class="QLabel" name="descriptionLabel">
- <property name="toolTip">
- <string notr="true"/>
- </property>
+ <item row="0" column="1">
+ <widget class="QLabel" name="nameLabel">
<property name="text">
<string notr="true"/>
</property>
@@ -82,28 +107,47 @@
</property>
</widget>
</item>
- <item row="0" column="0" rowspan="2">
- <widget class="QLabel" name="iconLabel">
- <property name="minimumSize">
- <size>
- <width>0</width>
- <height>0</height>
- </size>
+ <item row="2" column="1">
+ <widget class="QLabel" name="licenseLabel">
+ <property name="text">
+ <string/>
</property>
- <property name="maximumSize">
- <size>
- <width>64</width>
- <height>64</height>
- </size>
+ <property name="textFormat">
+ <enum>Qt::RichText</enum>
</property>
+ <property name="alignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="1">
+ <widget class="QLabel" name="issueTrackerLabel">
<property name="text">
- <string notr="true"/>
+ <string/>
</property>
- <property name="scaledContents">
- <bool>false</bool>
+ <property name="textFormat">
+ <enum>Qt::RichText</enum>
</property>
- <property name="margin">
- <number>0</number>
+ <property name="alignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
diff --git a/launcher/ui/widgets/JavaSettingsWidget.cpp b/launcher/ui/widgets/JavaSettingsWidget.cpp
index 15994319..c94fdd8d 100644
--- a/launcher/ui/widgets/JavaSettingsWidget.cpp
+++ b/launcher/ui/widgets/JavaSettingsWidget.cpp
@@ -46,7 +46,7 @@ void JavaSettingsWidget::setupUi()
m_verticalLayout = new QVBoxLayout(this);
m_verticalLayout->setObjectName(QStringLiteral("verticalLayout"));
- m_versionWidget = new VersionSelectWidget(this);
+ m_versionWidget = new VersionSelectWidget(true, this);
m_verticalLayout->addWidget(m_versionWidget);
m_horizontalLayout = new QHBoxLayout();
diff --git a/launcher/ui/widgets/LanguageSelectionWidget.cpp b/launcher/ui/widgets/LanguageSelectionWidget.cpp
index 256b09da..37d05347 100644
--- a/launcher/ui/widgets/LanguageSelectionWidget.cpp
+++ b/launcher/ui/widgets/LanguageSelectionWidget.cpp
@@ -1,16 +1,16 @@
#include "LanguageSelectionWidget.h"
-#include <QVBoxLayout>
-#include <QTreeView>
+#include <QCheckBox>
#include <QHeaderView>
#include <QLabel>
+#include <QTreeView>
+#include <QVBoxLayout>
#include "Application.h"
#include "BuildConfig.h"
-#include "translations/TranslationsModel.h"
#include "settings/Setting.h"
+#include "translations/TranslationsModel.h"
-LanguageSelectionWidget::LanguageSelectionWidget(QWidget *parent) :
- QWidget(parent)
+LanguageSelectionWidget::LanguageSelectionWidget(QWidget* parent) : QWidget(parent)
{
verticalLayout = new QVBoxLayout(this);
verticalLayout->setObjectName(QStringLiteral("verticalLayout"));
@@ -31,6 +31,13 @@ LanguageSelectionWidget::LanguageSelectionWidget(QWidget *parent) :
helpUsLabel->setWordWrap(true);
verticalLayout->addWidget(helpUsLabel);
+ formatCheckbox = new QCheckBox(this);
+ formatCheckbox->setObjectName(QStringLiteral("formatCheckbox"));
+ formatCheckbox->setCheckState(APPLICATION->settings()->get("UseSystemLocale").toBool() ? Qt::Checked : Qt::Unchecked);
+ connect(formatCheckbox, &QCheckBox::stateChanged,
+ [this]() { APPLICATION->translations()->setUseSystemLocale(formatCheckbox->isChecked()); });
+ verticalLayout->addWidget(formatCheckbox);
+
auto translations = APPLICATION->translations();
auto index = translations->selectedIndex();
languageView->setModel(translations.get());
@@ -38,7 +45,7 @@ LanguageSelectionWidget::LanguageSelectionWidget(QWidget *parent) :
languageView->header()->setSectionResizeMode(QHeaderView::ResizeToContents);
languageView->header()->setSectionResizeMode(0, QHeaderView::Stretch);
connect(languageView->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &LanguageSelectionWidget::languageRowChanged);
- verticalLayout->setContentsMargins(0,0,0,0);
+ verticalLayout->setContentsMargins(0, 0, 0, 0);
auto language_setting = APPLICATION->settings()->getSetting("Language");
connect(language_setting.get(), &Setting::SettingChanged, this, &LanguageSelectionWidget::languageSettingChanged);
@@ -53,15 +60,14 @@ QString LanguageSelectionWidget::getSelectedLanguageKey() const
void LanguageSelectionWidget::retranslate()
{
QString text = tr("Don't see your language or the quality is poor?<br/><a href=\"%1\">Help us with translations!</a>")
- .arg(BuildConfig.TRANSLATIONS_URL);
+ .arg(BuildConfig.TRANSLATIONS_URL);
helpUsLabel->setText(text);
-
+ formatCheckbox->setText(tr("Use system locales"));
}
void LanguageSelectionWidget::languageRowChanged(const QModelIndex& current, const QModelIndex& previous)
{
- if (current == previous)
- {
+ if (current == previous) {
return;
}
auto translations = APPLICATION->translations();
@@ -70,7 +76,7 @@ void LanguageSelectionWidget::languageRowChanged(const QModelIndex& current, con
translations->updateLanguage(key);
}
-void LanguageSelectionWidget::languageSettingChanged(const Setting &, const QVariant)
+void LanguageSelectionWidget::languageSettingChanged(const Setting&, const QVariant)
{
auto translations = APPLICATION->translations();
auto index = translations->selectedIndex();
diff --git a/launcher/ui/widgets/LanguageSelectionWidget.h b/launcher/ui/widgets/LanguageSelectionWidget.h
index 4a88924c..5e86a288 100644
--- a/launcher/ui/widgets/LanguageSelectionWidget.h
+++ b/launcher/ui/widgets/LanguageSelectionWidget.h
@@ -21,23 +21,24 @@ class QVBoxLayout;
class QTreeView;
class QLabel;
class Setting;
+class QCheckBox;
-class LanguageSelectionWidget: public QWidget
-{
+class LanguageSelectionWidget : public QWidget {
Q_OBJECT
-public:
- explicit LanguageSelectionWidget(QWidget *parent = 0);
- virtual ~LanguageSelectionWidget() { };
+ public:
+ explicit LanguageSelectionWidget(QWidget* parent = 0);
+ virtual ~LanguageSelectionWidget(){};
QString getSelectedLanguageKey() const;
void retranslate();
-protected slots:
- void languageRowChanged(const QModelIndex &current, const QModelIndex &previous);
- void languageSettingChanged(const Setting &, const QVariant);
+ protected slots:
+ void languageRowChanged(const QModelIndex& current, const QModelIndex& previous);
+ void languageSettingChanged(const Setting&, const QVariant);
-private:
- QVBoxLayout *verticalLayout = nullptr;
- QTreeView *languageView = nullptr;
- QLabel *helpUsLabel = nullptr;
+ private:
+ QVBoxLayout* verticalLayout = nullptr;
+ QTreeView* languageView = nullptr;
+ QLabel* helpUsLabel = nullptr;
+ QCheckBox* formatCheckbox = nullptr;
};
diff --git a/launcher/ui/widgets/ModListView.cpp b/launcher/ui/widgets/ModListView.cpp
index 09b03a76..80a918b6 100644
--- a/launcher/ui/widgets/ModListView.cpp
+++ b/launcher/ui/widgets/ModListView.cpp
@@ -14,9 +14,6 @@
*/
#include "ModListView.h"
-
-#include "minecraft/mod/ModFolderModel.h"
-
#include <QHeaderView>
#include <QMouseEvent>
#include <QPainter>
@@ -65,17 +62,13 @@ void ModListView::setModel ( QAbstractItemModel* model )
for(int i = 1; i < head->count(); i++)
head->setSectionResizeMode(i, QHeaderView::ResizeToContents);
}
+}
- auto real_model = model;
- if (auto proxy_model = dynamic_cast<QSortFilterProxyModel*>(model); proxy_model)
- real_model = proxy_model->sourceModel();
-
- if (auto mod_model = dynamic_cast<ModFolderModel*>(real_model); mod_model) {
- connect(mod_model, &ModFolderModel::updateFinished, this, [this, mod_model]{
- auto mods = mod_model->allMods();
- // Hide the 'Provider' column if no mod has a defined provider!
- setColumnHidden(ModFolderModel::Columns::ProviderColumn,
- std::none_of(mods.constBegin(), mods.constEnd(), [](auto const mod){ return mod->provider().has_value(); }));
- });
+void ModListView::setResizeModes(const QList<QHeaderView::ResizeMode> &modes)
+{
+ auto head = header();
+ for(int i = 0; i < modes.count(); i++) {
+ head->setSectionResizeMode(i, modes[i]);
}
}
+
diff --git a/launcher/ui/widgets/ModListView.h b/launcher/ui/widgets/ModListView.h
index 881e092f..3f0b3b0e 100644
--- a/launcher/ui/widgets/ModListView.h
+++ b/launcher/ui/widgets/ModListView.h
@@ -14,6 +14,7 @@
*/
#pragma once
+#include <QHeaderView>
#include <QTreeView>
class ModListView: public QTreeView
@@ -22,4 +23,5 @@ class ModListView: public QTreeView
public:
explicit ModListView ( QWidget* parent = 0 );
virtual void setModel ( QAbstractItemModel* model );
+ virtual void setResizeModes (const QList<QHeaderView::ResizeMode>& modes);
};
diff --git a/launcher/ui/widgets/PageContainer.cpp b/launcher/ui/widgets/PageContainer.cpp
index 0a06a351..b98c9796 100644
--- a/launcher/ui/widgets/PageContainer.cpp
+++ b/launcher/ui/widgets/PageContainer.cpp
@@ -87,10 +87,16 @@ PageContainer::PageContainer(BasePageProvider *pageProvider, QString defaultId,
auto pages = pageProvider->getPages();
for (auto page : pages)
{
- page->stackIndex = m_pageStack->addWidget(dynamic_cast<QWidget *>(page));
+ auto widget = dynamic_cast<QWidget *>(page);
+ widget->setParent(this);
+ page->stackIndex = m_pageStack->addWidget(widget);
page->listIndex = counter;
page->setParentContainer(this);
counter++;
+ page->updateExtraInfo = [this](QString id, QString info) {
+ if (m_currentPage && id == m_currentPage->id())
+ m_header->setText(m_currentPage->displayName() + info);
+ };
}
m_model->setPages(pages);
@@ -135,6 +141,11 @@ BasePage* PageContainer::getPage(QString pageId)
return m_model->findPageEntryById(pageId);
}
+const QList<BasePage*> PageContainer::getPages() const
+{
+ return m_model->pages();
+}
+
void PageContainer::refreshContainer()
{
m_proxyModel->invalidate();
diff --git a/launcher/ui/widgets/PageContainer.h b/launcher/ui/widgets/PageContainer.h
index 97e294dc..ad74d43a 100644
--- a/launcher/ui/widgets/PageContainer.h
+++ b/launcher/ui/widgets/PageContainer.h
@@ -80,6 +80,7 @@ public:
virtual bool selectPage(QString pageId) override;
BasePage* getPage(QString pageId) override;
+ const QList<BasePage*> getPages() const;
void refreshContainer() override;
virtual void setParentContainer(BasePageContainer * container)
diff --git a/launcher/ui/widgets/ProgressWidget.cpp b/launcher/ui/widgets/ProgressWidget.cpp
index f736af08..9181de7f 100644
--- a/launcher/ui/widgets/ProgressWidget.cpp
+++ b/launcher/ui/widgets/ProgressWidget.cpp
@@ -51,6 +51,7 @@ void ProgressWidget::watch(const Task* task)
connect(m_task, &Task::finished, this, &ProgressWidget::handleTaskFinish);
connect(m_task, &Task::status, this, &ProgressWidget::handleTaskStatus);
+ // TODO: should we connect &Task::details
connect(m_task, &Task::progress, this, &ProgressWidget::handleTaskProgress);
connect(m_task, &Task::destroyed, this, &ProgressWidget::taskDestroyed);
diff --git a/launcher/ui/widgets/ProjectItem.cpp b/launcher/ui/widgets/ProjectItem.cpp
index d1ff9dbc..0085d6b2 100644
--- a/launcher/ui/widgets/ProjectItem.cpp
+++ b/launcher/ui/widgets/ProjectItem.cpp
@@ -64,6 +64,17 @@ void ProjectItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& o
font.setBold(true);
font.setUnderline(true);
}
+ if (index.data(UserDataTypes::INSTALLED).toBool()) {
+ auto hRect = opt.rect;
+ hRect.setX(hRect.x() + 1);
+ hRect.setY(hRect.y() + 1);
+ hRect.setHeight(hRect.height() - 2);
+ hRect.setWidth(hRect.width() - 2);
+ // Set nice font
+ font.setItalic(true);
+ font.setOverline(true);
+ painter->drawRect(hRect);
+ }
font.setPointSize(font.pointSize() + 2);
painter->setFont(font);
diff --git a/launcher/ui/widgets/ProjectItem.h b/launcher/ui/widgets/ProjectItem.h
index f668edf6..196055ea 100644
--- a/launcher/ui/widgets/ProjectItem.h
+++ b/launcher/ui/widgets/ProjectItem.h
@@ -6,7 +6,8 @@
enum UserDataTypes {
TITLE = 257, // QString
DESCRIPTION = 258, // QString
- SELECTED = 259 // bool
+ SELECTED = 259, // bool
+ INSTALLED = 260 // bool
};
/** This is an item delegate composed of:
diff --git a/launcher/ui/widgets/SubTaskProgressBar.cpp b/launcher/ui/widgets/SubTaskProgressBar.cpp
new file mode 100644
index 00000000..84ea5f20
--- /dev/null
+++ b/launcher/ui/widgets/SubTaskProgressBar.cpp
@@ -0,0 +1,58 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * PrismLaucher - Minecraft Launcher
+ * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "SubTaskProgressBar.h"
+#include "ui_SubTaskProgressBar.h"
+
+unique_qobject_ptr<SubTaskProgressBar> SubTaskProgressBar::create(QWidget* parent)
+{
+ auto progress_bar = new SubTaskProgressBar(parent);
+ return unique_qobject_ptr<SubTaskProgressBar>(progress_bar);
+}
+
+SubTaskProgressBar::SubTaskProgressBar(QWidget* parent)
+ : ui(new Ui::SubTaskProgressBar)
+{
+ ui->setupUi(this);
+}
+SubTaskProgressBar::~SubTaskProgressBar()
+{
+ delete ui;
+}
+
+void SubTaskProgressBar::setRange(int min, int max)
+{
+ ui->progressBar->setRange(min, max);
+}
+
+void SubTaskProgressBar::setValue(int value)
+{
+ ui->progressBar->setValue(value);
+}
+
+void SubTaskProgressBar::setStatus(QString status)
+{
+ ui->statusLabel->setText(status);
+}
+
+void SubTaskProgressBar::setDetails(QString details)
+{
+ ui->statusDetailsLabel->setText(details);
+}
+
diff --git a/launcher/ui/widgets/SubTaskProgressBar.h b/launcher/ui/widgets/SubTaskProgressBar.h
new file mode 100644
index 00000000..8f8aeea2
--- /dev/null
+++ b/launcher/ui/widgets/SubTaskProgressBar.h
@@ -0,0 +1,48 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * PrismLaucher - Minecraft Launcher
+ * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.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 <QWidget>
+#include "QObjectPtr.h"
+
+namespace Ui {
+class SubTaskProgressBar;
+}
+
+class SubTaskProgressBar : public QWidget
+{
+ Q_OBJECT
+
+public:
+ static unique_qobject_ptr<SubTaskProgressBar> create(QWidget* parent = nullptr);
+
+ SubTaskProgressBar(QWidget* parent = nullptr);
+ ~SubTaskProgressBar();
+
+ void setRange(int min, int max);
+ void setValue(int value);
+ void setStatus(QString status);
+ void setDetails(QString details);
+
+
+
+private:
+ Ui::SubTaskProgressBar* ui;
+
+};
diff --git a/launcher/ui/widgets/SubTaskProgressBar.ui b/launcher/ui/widgets/SubTaskProgressBar.ui
new file mode 100644
index 00000000..5431eab6
--- /dev/null
+++ b/launcher/ui/widgets/SubTaskProgressBar.ui
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>SubTaskProgressBar</class>
+ <widget class="QWidget" name="SubTaskProgressBar">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>312</width>
+ <height>86</height>
+ </rect>
+ </property>
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="windowTitle">
+ <string>Form</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout" stretch="0,0">
+ <property name="spacing">
+ <number>0</number>
+ </property>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout" stretch="1,0">
+ <property name="spacing">
+ <number>8</number>
+ </property>
+ <item>
+ <widget class="QLabel" name="statusLabel">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="font">
+ <font>
+ <pointsize>8</pointsize>
+ </font>
+ </property>
+ <property name="text">
+ <string>Sub Task Status...</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="statusDetailsLabel">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="font">
+ <font>
+ <pointsize>8</pointsize>
+ </font>
+ </property>
+ <property name="text">
+ <string>Status Details</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QProgressBar" name="progressBar">
+ <property name="font">
+ <font>
+ <pointsize>8</pointsize>
+ </font>
+ </property>
+ <property name="value">
+ <number>24</number>
+ </property>
+ <property name="textVisible">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/ui/widgets/VersionListView.cpp b/launcher/ui/widgets/VersionListView.cpp
index 0e126c65..06e58d22 100644
--- a/launcher/ui/widgets/VersionListView.cpp
+++ b/launcher/ui/widgets/VersionListView.cpp
@@ -1,7 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
- * PolyMC - Minecraft Launcher
+ * Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ * Copyright (C) 2023 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
@@ -125,14 +126,9 @@ void VersionListView::paintEvent(QPaintEvent *event)
QString VersionListView::currentEmptyString() const
{
- if(m_itemCount) {
- return QString();
- }
switch(m_emptyMode)
{
default:
- case VersionListView::Empty:
- return QString();
case VersionListView::String:
return m_emptyString;
case VersionListView::ErrorString:
diff --git a/launcher/ui/widgets/VersionSelectWidget.cpp b/launcher/ui/widgets/VersionSelectWidget.cpp
index 404860d9..a956ddb3 100644
--- a/launcher/ui/widgets/VersionSelectWidget.cpp
+++ b/launcher/ui/widgets/VersionSelectWidget.cpp
@@ -1,15 +1,20 @@
#include "VersionSelectWidget.h"
+#include <QApplication>
+#include <QEvent>
+#include <QHeaderView>
+#include <QKeyEvent>
#include <QProgressBar>
#include <QVBoxLayout>
-#include <QHeaderView>
#include "VersionProxyModel.h"
#include "ui/dialogs/CustomMessageBox.h"
-VersionSelectWidget::VersionSelectWidget(QWidget* parent)
- : QWidget(parent)
+VersionSelectWidget::VersionSelectWidget(QWidget* parent) : VersionSelectWidget(false, parent) {}
+
+VersionSelectWidget::VersionSelectWidget(bool focusSearch, QWidget* parent)
+ : QWidget(parent), focusSearch(focusSearch)
{
setObjectName(QStringLiteral("VersionSelectWidget"));
verticalLayout = new QVBoxLayout(this);
@@ -30,6 +35,21 @@ VersionSelectWidget::VersionSelectWidget(QWidget* parent)
listView->setModel(m_proxyModel);
verticalLayout->addWidget(listView);
+ search = new QLineEdit(this);
+ search->setPlaceholderText(tr("Search"));
+ search->setClearButtonEnabled(true);
+ verticalLayout->addWidget(search);
+ connect(search, &QLineEdit::textEdited, [this](const QString& value) {
+ m_proxyModel->setSearch(value);
+ if (!value.isEmpty() || !listView->selectionModel()->hasSelection()) {
+ const QModelIndex first = listView->model()->index(0, 0);
+ listView->selectionModel()->setCurrentIndex(first, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
+ listView->scrollToTop();
+ } else
+ listView->scrollTo(listView->selectionModel()->currentIndex(), QAbstractItemView::PositionAtCenter);
+ });
+ search->installEventFilter(this);
+
sneakyProgressBar = new QProgressBar(this);
sneakyProgressBar->setObjectName(QStringLiteral("sneakyProgressBar"));
sneakyProgressBar->setFormat(QStringLiteral("%p%"));
@@ -72,6 +92,23 @@ void VersionSelectWidget::setResizeOn(int column)
listView->header()->setSectionResizeMode(resizeOnColumn, QHeaderView::Stretch);
}
+bool VersionSelectWidget::eventFilter(QObject *watched, QEvent *event) {
+ if (watched == search && event->type() == QEvent::KeyPress) {
+ const QKeyEvent* keyEvent = (QKeyEvent*)event;
+ const bool up = keyEvent->key() == Qt::Key_Up;
+ const bool down = keyEvent->key() == Qt::Key_Down;
+ if (up || down) {
+ const QModelIndex index = listView->model()->index(listView->currentIndex().row() + (up ? -1 : 1), 0);
+ if (index.row() >= 0 && index.row() < listView->model()->rowCount()) {
+ listView->selectionModel()->setCurrentIndex(index, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
+ return true;
+ }
+ }
+ }
+
+ return QObject::eventFilter(watched, event);
+}
+
void VersionSelectWidget::initialize(BaseVersionList *vlist)
{
m_vlist = vlist;
@@ -79,6 +116,9 @@ void VersionSelectWidget::initialize(BaseVersionList *vlist)
listView->header()->setSectionResizeMode(QHeaderView::ResizeToContents);
listView->header()->setSectionResizeMode(resizeOnColumn, QHeaderView::Stretch);
+ if (focusSearch)
+ search->setFocus();
+
if (!m_vlist->isLoaded())
{
loadList();
diff --git a/launcher/ui/widgets/VersionSelectWidget.h b/launcher/ui/widgets/VersionSelectWidget.h
index e75efc6f..be4ba768 100644
--- a/launcher/ui/widgets/VersionSelectWidget.h
+++ b/launcher/ui/widgets/VersionSelectWidget.h
@@ -1,22 +1,43 @@
-/* Copyright 2013-2021 MultiMC Contributors
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
*
- * 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
+ * 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.
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
*
- * Unless required by applicable law or agreed to in writing, software
- * 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.
+ * 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 <QWidget>
#include <QSortFilterProxyModel>
+#include <QLineEdit>
#include "BaseVersionList.h"
#include "VersionListView.h"
@@ -30,7 +51,8 @@ class VersionSelectWidget: public QWidget
{
Q_OBJECT
public:
- explicit VersionSelectWidget(QWidget *parent = 0);
+ explicit VersionSelectWidget(QWidget *parent);
+ explicit VersionSelectWidget(bool focusSearch = false, QWidget *parent = 0);
~VersionSelectWidget();
//! loads the list if needed.
@@ -52,6 +74,7 @@ public:
void setEmptyErrorString(QString emptyErrorString);
void setEmptyMode(VersionListView::EmptyMode mode);
void setResizeOn(int column);
+ bool eventFilter(QObject* watched, QEvent* event) override;
signals:
void selectedVersionChanged(BaseVersion::Ptr version);
@@ -75,9 +98,10 @@ private:
int resizeOnColumn = 0;
Task * loadTask;
bool preselectedAlready = false;
+ bool focusSearch;
-private:
QVBoxLayout *verticalLayout = nullptr;
VersionListView *listView = nullptr;
+ QLineEdit *search;
QProgressBar *sneakyProgressBar = nullptr;
};
diff --git a/launcher/ui/widgets/WideBar.cpp b/launcher/ui/widgets/WideBar.cpp
index ac34e3aa..a77c45fe 100644
--- a/launcher/ui/widgets/WideBar.cpp
+++ b/launcher/ui/widgets/WideBar.cpp
@@ -116,12 +116,21 @@ void WideBar::insertActionAfter(QAction* after, QAction* action)
if (iter == m_entries.end())
return;
+ iter++;
+ // the action to insert after is present
+ // however, the element after it isn't valid
+ if (iter == m_entries.end()) {
+ // append the action instead of inserting it
+ addAction(action);
+ return;
+ }
+
BarEntry entry;
- entry.bar_action = insertWidget((iter + 1)->bar_action, new ActionButton(action, this, m_use_default_action));
+ entry.bar_action = insertWidget(iter->bar_action, new ActionButton(action, this, m_use_default_action));
entry.menu_action = action;
entry.type = BarEntry::Type::Action;
- m_entries.insert(iter + 1, entry);
+ m_entries.insert(iter, entry);
m_menu_state = MenuState::Dirty;
}