aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/build.yml29
-rw-r--r--.github/workflows/trigger_release.yml2
-rw-r--r--CMakeLists.txt12
-rw-r--r--README.md6
-rw-r--r--buildconfig/BuildConfig.cpp.in1
-rw-r--r--buildconfig/BuildConfig.h5
-rw-r--r--default.nix15
-rw-r--r--flake.lock62
-rw-r--r--flake.nix78
-rw-r--r--launcher/Application.cpp85
-rw-r--r--launcher/Application.h5
-rw-r--r--launcher/CMakeLists.txt71
-rw-r--r--launcher/FastFileIconProvider.cpp47
-rw-r--r--launcher/FastFileIconProvider.h26
-rw-r--r--launcher/FileIgnoreProxy.cpp256
-rw-r--r--launcher/FileIgnoreProxy.h72
-rw-r--r--launcher/FileSystem.cpp6
-rw-r--r--launcher/InstanceCopyTask.cpp30
-rw-r--r--launcher/InstanceCreationTask.h2
-rw-r--r--launcher/InstanceImportTask.cpp27
-rw-r--r--launcher/InstanceList.cpp2
-rw-r--r--launcher/InstanceTask.cpp5
-rw-r--r--launcher/JavaCommon.cpp6
-rw-r--r--launcher/LoggedProcess.cpp8
-rw-r--r--launcher/MMCTime.cpp64
-rw-r--r--launcher/MMCTime.h9
-rw-r--r--launcher/MTPixmapCache.h41
-rw-r--r--launcher/ResourceDownloadTask.cpp50
-rw-r--r--launcher/ResourceDownloadTask.h60
-rw-r--r--launcher/StringUtils.cpp67
-rw-r--r--launcher/StringUtils.h12
-rw-r--r--launcher/VersionProxyModel.cpp29
-rw-r--r--launcher/VersionProxyModel.h3
-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/Update.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.cpp33
-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/VersionFile.h2
-rw-r--r--launcher/minecraft/WorldList.cpp5
-rw-r--r--launcher/minecraft/WorldList.h4
-rw-r--r--launcher/minecraft/auth/AccountList.cpp6
-rw-r--r--launcher/minecraft/auth/AuthRequest.cpp22
-rw-r--r--launcher/minecraft/auth/MinecraftAccount.cpp16
-rw-r--r--launcher/minecraft/auth/steps/XboxUserStep.cpp4
-rw-r--r--launcher/minecraft/mod/DataPack.cpp4
-rw-r--r--launcher/minecraft/mod/Mod.cpp57
-rw-r--r--launcher/minecraft/mod/Mod.h22
-rw-r--r--launcher/minecraft/mod/ModDetails.h97
-rw-r--r--launcher/minecraft/mod/ModFolderModel.cpp20
-rw-r--r--launcher/minecraft/mod/ModFolderModel.h5
-rw-r--r--launcher/minecraft/mod/ResourceFolderModel.cpp78
-rw-r--r--launcher/minecraft/mod/ResourceFolderModel.h19
-rw-r--r--launcher/minecraft/mod/ResourcePack.cpp22
-rw-r--r--launcher/minecraft/mod/ResourcePack.h6
-rw-r--r--launcher/minecraft/mod/ResourcePackFolderModel.cpp29
-rw-r--r--launcher/minecraft/mod/ResourcePackFolderModel.h5
-rw-r--r--launcher/minecraft/mod/ShaderPackFolderModel.h4
-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.h23
-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.cpp211
-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/LocalTexturePackParseTask.cpp67
-rw-r--r--launcher/minecraft/mod/tasks/LocalTexturePackParseTask.h5
-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.cpp25
-rw-r--r--launcher/modplatform/ModIndex.h64
-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.cpp130
-rw-r--r--launcher/modplatform/flame/FileResolvingTask.h36
-rw-r--r--launcher/modplatform/flame/FlameAPI.cpp51
-rw-r--r--launcher/modplatform/flame/FlameAPI.h54
-rw-r--r--launcher/modplatform/flame/FlameCheckUpdate.cpp26
-rw-r--r--launcher/modplatform/flame/FlameInstanceCreationTask.cpp17
-rw-r--r--launcher/modplatform/flame/FlameModIndex.cpp81
-rw-r--r--launcher/modplatform/flame/FlameModIndex.h6
-rw-r--r--launcher/modplatform/flame/FlamePackIndex.h10
-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.cpp1
-rw-r--r--launcher/modplatform/modrinth/ModrinthAPI.cpp22
-rw-r--r--launcher/modplatform/modrinth/ModrinthAPI.h41
-rw-r--r--launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp33
-rw-r--r--launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp11
-rw-r--r--launcher/modplatform/modrinth/ModrinthPackExportTask.cpp319
-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/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/screenshots/ImgurAlbumCreation.cpp10
-rw-r--r--launcher/screenshots/ImgurUpload.cpp10
-rw-r--r--launcher/settings/INIFile.cpp109
-rw-r--r--launcher/settings/SettingsObject.cpp9
-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.cpp7
-rw-r--r--launcher/ui/MainWindow.cpp76
-rw-r--r--launcher/ui/MainWindow.h9
-rw-r--r--launcher/ui/MainWindow.ui34
-rw-r--r--launcher/ui/dialogs/AboutDialog.cpp11
-rw-r--r--launcher/ui/dialogs/ExportInstanceDialog.cpp304
-rw-r--r--launcher/ui/dialogs/ExportInstanceDialog.h46
-rw-r--r--launcher/ui/dialogs/ExportMrPackDialog.cpp124
-rw-r--r--launcher/ui/dialogs/ExportMrPackDialog.h45
-rw-r--r--launcher/ui/dialogs/ExportMrPackDialog.ui136
-rw-r--r--launcher/ui/dialogs/NewInstanceDialog.cpp2
-rw-r--r--launcher/ui/dialogs/NewsDialog.cpp2
-rw-r--r--launcher/ui/dialogs/ProgressDialog.cpp136
-rw-r--r--launcher/ui/dialogs/ProgressDialog.h54
-rw-r--r--launcher/ui/dialogs/ProgressDialog.ui150
-rw-r--r--launcher/ui/dialogs/ResourceDownloadDialog.cpp162
-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/pages/BasePage.h24
-rw-r--r--launcher/ui/pages/global/APIPage.cpp2
-rw-r--r--launcher/ui/pages/global/LauncherPage.ui8
-rw-r--r--launcher/ui/pages/global/MinecraftPage.ui2
-rw-r--r--launcher/ui/pages/instance/ExternalResourcesPage.cpp71
-rw-r--r--launcher/ui/pages/instance/ExternalResourcesPage.h2
-rw-r--r--launcher/ui/pages/instance/ExternalResourcesPage.ui3
-rw-r--r--launcher/ui/pages/instance/InstanceSettingsPage.cpp66
-rw-r--r--launcher/ui/pages/instance/InstanceSettingsPage.h5
-rw-r--r--launcher/ui/pages/instance/InstanceSettingsPage.ui17
-rw-r--r--launcher/ui/pages/instance/ManagedPackPage.cpp43
-rw-r--r--launcher/ui/pages/instance/ModFolderPage.cpp27
-rw-r--r--launcher/ui/pages/instance/VersionPage.cpp4
-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.cpp107
-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/legacy_ftb/ListModel.cpp111
-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/themes/SystemTheme.cpp2
-rw-r--r--launcher/ui/widgets/InfoFrame.cpp122
-rw-r--r--launcher/ui/widgets/InfoFrame.h4
-rw-r--r--launcher/ui/widgets/InfoFrame.ui92
-rw-r--r--launcher/ui/widgets/JavaSettingsWidget.cpp2
-rw-r--r--launcher/ui/widgets/ModListView.cpp9
-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--libraries/katabasis/src/Reply.cpp2
-rw-r--r--nix/default.nix118
-rw-r--r--nix/dev.nix46
-rw-r--r--nix/distribution.nix29
-rw-r--r--nix/flake-compat.nix9
-rw-r--r--nix/package.nix65
-rw-r--r--program_info/org.prismlauncher.PrismLauncher.desktop.in2
-rw-r--r--program_info/org.prismlauncher.PrismLauncher.metainfo.xml.in2
-rw-r--r--program_info/win_install.nsi.in76
-rw-r--r--renovate.json8
-rw-r--r--tests/INIFile_test.cpp147
-rw-r--r--tests/ResourceFolderModel_test.cpp17
-rw-r--r--tests/ResourceModel_test.cpp6
-rw-r--r--tests/Task_test.cpp9
257 files changed, 6820 insertions, 2422 deletions
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 02cc8b1f..c2966abe 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -68,7 +68,7 @@ jobs:
qt_ver: 6
qt_host: windows
qt_arch: ''
- qt_version: '6.5.0'
+ qt_version: '6.5.1'
qt_modules: 'qt5compat qtimageformats'
qt_tools: ''
@@ -80,13 +80,13 @@ jobs:
qt_ver: 6
qt_host: windows
qt_arch: 'win64_msvc2019_arm64'
- qt_version: '6.5.0'
+ qt_version: '6.5.1'
qt_modules: 'qt5compat qtimageformats'
qt_tools: ''
- os: macos-12
name: macOS
- macosx_deployment_target: 10.15
+ macosx_deployment_target: 11.0
qt_ver: 6
qt_host: mac
qt_arch: ''
@@ -191,7 +191,7 @@ jobs:
if: runner.os == 'Linux'
run: |
sudo apt-get -y update
- sudo apt-get -y install ninja-build extra-cmake-modules scdoc
+ sudo apt-get -y install ninja-build extra-cmake-modules scdoc appstream
- name: Install Dependencies (macOS)
if: runner.os == 'macOS'
@@ -250,6 +250,7 @@ jobs:
wget "https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage"
${{ github.workspace }}/.github/scripts/prepare_JREs.sh
+ sudo apt install libopengl0
- name: Add QT_HOST_PATH var (Windows MSVC arm64)
if: runner.os == 'Windows' && matrix.architecture == 'arm64'
@@ -375,6 +376,8 @@ jobs:
shell: msys2 {0}
run: |
cmake --install ${{ env.BUILD_DIR }}
+ touch ${{ env.INSTALL_DIR }}/manifest.txt
+ for l in $(find ${{ env.INSTALL_DIR }} -type f); do l=$(cygpath -u $l); l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_DIR }}/}; l=${l#./}; echo $l; done >> ${{ env.INSTALL_DIR }}/manifest.txt
- name: Package (Windows MSVC)
if: runner.os == 'Windows' && matrix.msystem == ''
@@ -387,6 +390,10 @@ jobs:
Copy-Item D:/a/PrismLauncher/Qt/Tools/OpenSSL/Win_x86/bin/libcrypto-1_1.dll -Destination libcrypto-1_1.dll
Copy-Item D:/a/PrismLauncher/Qt/Tools/OpenSSL/Win_x86/bin/libssl-1_1.dll -Destination libssl-1_1.dll
}
+ cd ${{ github.workspace }}
+
+ Get-ChildItem ${{ env.INSTALL_DIR }} -Recurse | ForEach FullName | Resolve-Path -Relative | %{ $_.TrimStart('.\') } | %{ $_.TrimStart('${{ env.INSTALL_DIR }}') } | %{ $_.TrimStart('\') } | Out-File -FilePath ${{ env.INSTALL_DIR }}/manifest.txt
+
- name: Fetch codesign certificate (Windows)
if: runner.os == 'Windows'
@@ -411,12 +418,15 @@ jobs:
run: |
cp -r ${{ env.INSTALL_DIR }} ${{ env.INSTALL_PORTABLE_DIR }} # cmake install on Windows is slow, let's just copy instead
cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable
+ for l in $(find ${{ env.INSTALL_PORTABLE_DIR }} -type f); do l=$(cygpath -u $l); l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_PORTABLE_DIR }}/}; l=${l#./}; echo $l; done >> ${{ env.INSTALL_PORTABLE_DIR }}/manifest.txt
- name: Package (Windows MSVC, portable)
if: runner.os == 'Windows' && matrix.msystem == ''
run: |
cp -r ${{ env.INSTALL_DIR }} ${{ env.INSTALL_PORTABLE_DIR }} # cmake install on Windows is slow, let's just copy instead
cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable
+
+ Get-ChildItem ${{ env.INSTALL_PORTABLE_DIR }} -Recurse | ForEach FullName | Resolve-Path -Relative | %{ $_.TrimStart('.\') } | %{ $_.TrimStart('${{ env.INSTALL_PORTABLE_DIR }}') } | %{ $_.TrimStart('\') } | Out-File -FilePath ${{ env.INSTALL_DIR }}/manifest.txt
- name: Package (Windows, installer)
if: runner.os == 'Windows'
@@ -437,6 +447,7 @@ jobs:
if: runner.os == 'Linux'
run: |
cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_DIR }}
+ for l in $(find ${{ env.INSTALL_DIR }} -type f); do l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_DIR }}/}; l=${l#./}; echo $l; done > ${{ env.INSTALL_DIR }}/manifest.txt
cd ${{ env.INSTALL_DIR }}
tar --owner root --group root -czf ../PrismLauncher.tar.gz *
@@ -446,6 +457,8 @@ jobs:
run: |
cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }}
cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable
+ for l in $(find ${{ env.INSTALL_PORTABLE_DIR }} -type f); do l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_PORTABLE_DIR }}/}; l=${l#./}; echo $l; done > ${{ env.INSTALL_PORTABLE_DIR }}/manifest.txt
+
cd ${{ env.INSTALL_PORTABLE_DIR }}
tar -czf ../PrismLauncher-portable.tar.gz *
@@ -455,7 +468,8 @@ jobs:
shell: bash
run: |
cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_APPIMAGE_DIR }}/usr
-
+ mv ${{ env.INSTALL_APPIMAGE_DIR }}/usr/share/metainfo/org.prismlauncher.PrismLauncher.metainfo.xml ${{ env.INSTALL_APPIMAGE_DIR }}/usr/share/metainfo/org.prismlauncher.PrismLauncher.appdata.xml
+ export "NO_APPSTREAM=1" # we have to skip appstream checking because appstream on ubuntu 20.04 is outdated
export OUTPUT="PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage"
chmod +x linuxdeploy-*.AppImage
@@ -470,7 +484,8 @@ jobs:
cp -r /home/runner/work/PrismLauncher/Qt/${{ matrix.qt_version }}/gcc_64/plugins/iconengines/* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/plugins/iconengines
cp /usr/lib/x86_64-linux-gnu/libcrypto.so.1.1 ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/
- cp /usr/lib/x86_64-linux-gnu/libssl.so.1.1 ${{ env.INSTALL_APPIMAGE_DIR }}//usr/lib/
+ cp /usr/lib/x86_64-linux-gnu/libssl.so.1.1 ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/
+ cp /usr/lib/x86_64-linux-gnu/libOpenGL.so.0* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/
LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib"
LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/jvm/java-8-openjdk/lib/amd64/server"
@@ -587,7 +602,7 @@ jobs:
submodules: 'true'
- name: Install nix
if: inputs.build_type == 'Debug'
- uses: cachix/install-nix-action@v20
+ uses: cachix/install-nix-action@v22
with:
install_url: https://nixos.org/nix/install
extra_nix_config: |
diff --git a/.github/workflows/trigger_release.yml b/.github/workflows/trigger_release.yml
index 3c56a38e..f19b8398 100644
--- a/.github/workflows/trigger_release.yml
+++ b/.github/workflows/trigger_release.yml
@@ -47,7 +47,7 @@ jobs:
mv PrismLauncher-macOS-Legacy*/PrismLauncher.tar.gz PrismLauncher-macOS-Legacy-${{ env.VERSION }}.tar.gz
mv PrismLauncher-macOS*/PrismLauncher.tar.gz PrismLauncher-macOS-${{ env.VERSION }}.tar.gz
- tar -czf PrismLauncher-${{ env.VERSION }}.tar.gz PrismLauncher-${{ env.VERSION }}
+ tar --exclude='.git' -czf PrismLauncher-${{ env.VERSION }}.tar.gz PrismLauncher-${{ env.VERSION }}
for d in PrismLauncher-Windows-MSVC*; do
cd "${d}" || continue
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 05c69c89..70a55319 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -138,7 +138,7 @@ set(Launcher_NEWS_OPEN_URL "https://prismlauncher.org/news" CACHE STRING "URL th
set(Launcher_HELP_URL "https://prismlauncher.org/wiki/help-pages/%1" CACHE STRING "URL (with arg %1 to be substituted with page-id) that gets opened when the user requests help")
######## Set version numbers ########
-set(Launcher_VERSION_MAJOR 7)
+set(Launcher_VERSION_MAJOR 8)
set(Launcher_VERSION_MINOR 0)
set(Launcher_VERSION_NAME "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}")
@@ -164,13 +164,13 @@ set(Launcher_BUG_TRACKER_URL "https://github.com/PrismLauncher/PrismLauncher/iss
set(Launcher_TRANSLATIONS_URL "https://hosted.weblate.org/projects/prismlauncher/launcher/" CACHE STRING "URL for the translations platform.")
# Matrix Space
-set(Launcher_MATRIX_URL "https://matrix.to/#/#prismlauncher:matrix.org" CACHE STRING "URL to the Matrix Space")
+set(Launcher_MATRIX_URL "https://prismlauncher.org/matrix" CACHE STRING "URL to the Matrix Space")
# Discord URL
-set(Launcher_DISCORD_URL "https://discord.gg/prismlauncher" CACHE STRING "URL for the Discord guild.")
+set(Launcher_DISCORD_URL "https://prismlauncher.org/discord" CACHE STRING "URL for the Discord guild.")
# Subreddit URL
-set(Launcher_SUBREDDIT_URL "https://www.reddit.com/r/PrismLauncher/" CACHE STRING "URL for the subreddit.")
+set(Launcher_SUBREDDIT_URL "https://prismlauncher.org/reddit" CACHE STRING "URL for the subreddit.")
# Builds
set(Launcher_FORCE_BUNDLED_LIBS OFF CACHE BOOL "Prevent using system libraries, if they are available as submodules")
@@ -345,6 +345,8 @@ elseif(UNIX)
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_SVG} DESTINATION "${KDE_INSTALL_ICONDIR}/hicolor/scalable/apps")
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_mrpack_MIMEInfo} DESTINATION ${KDE_INSTALL_MIMEDIR})
+ install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/launcher/qtlogging.ini" DESTINATION "${KDE_INSTALL_DATADIR}/${Launcher_Name}")
+
if(Launcher_ManPage)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_ManPage} DESTINATION "${KDE_INSTALL_MANDIR}/man6")
endif()
@@ -372,6 +374,8 @@ else()
message(FATAL_ERROR "Platform not supported")
endif()
+
+
################################ Included Libs ################################
include(ExternalProject)
diff --git a/README.md b/README.md
index aaa1fd4c..993f02f5 100644
--- a/README.md
+++ b/README.md
@@ -38,15 +38,15 @@ Feel free to create a GitHub issue if you find a bug or want to suggest a new fe
- **Our Discord server:**
-[![Prism Launcher Discord server](https://discordapp.com/api/guilds/1031648380885147709/widget.png?style=banner3)](https://discord.gg/prismlauncher)
+[![Prism Launcher Discord server](https://discordapp.com/api/guilds/1031648380885147709/widget.png?style=banner3)](https://prismlauncher.org/discord)
- **Our Matrix space:**
-[![PrismLauncher Space](https://img.shields.io/matrix/prismlauncher:matrix.org?style=for-the-badge&label=Matrix%20Space&logo=matrix&color=purple)](https://matrix.to/#/#prismlauncher:matrix.org)
+[![PrismLauncher Space](https://img.shields.io/matrix/prismlauncher:matrix.org?style=for-the-badge&label=Matrix%20Space&logo=matrix&color=purple)](https://prismlauncher.org/matrix)
- **Our Subreddit:**
-[![r/PrismLauncher](https://img.shields.io/reddit/subreddit-subscribers/prismlauncher?style=for-the-badge&logo=reddit)](https://www.reddit.com/r/PrismLauncher/)
+[![r/PrismLauncher](https://img.shields.io/reddit/subreddit-subscribers/prismlauncher?style=for-the-badge&logo=reddit)](https://prismlauncher.org/reddit)
## Translations
diff --git a/buildconfig/BuildConfig.cpp.in b/buildconfig/BuildConfig.cpp.in
index 35373189..8a412b7f 100644
--- a/buildconfig/BuildConfig.cpp.in
+++ b/buildconfig/BuildConfig.cpp.in
@@ -82,6 +82,7 @@ Config::Config()
{
GIT_REFSPEC = "refs/heads/stable";
GIT_TAG = versionString();
+ GIT_COMMIT = "";
}
if (GIT_REFSPEC.startsWith("refs/heads/"))
diff --git a/buildconfig/BuildConfig.h b/buildconfig/BuildConfig.h
index a05d7a9e..8543d724 100644
--- a/buildconfig/BuildConfig.h
+++ b/buildconfig/BuildConfig.h
@@ -1,8 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
- * PolyMC - Minecraft Launcher
+ * Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
* 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
@@ -36,6 +37,7 @@
#pragma once
#include <QString>
+#include <QList>
/**
* \brief The Config class holds all the build-time information passed from the build system.
@@ -160,6 +162,7 @@ class Config {
QString MODRINTH_STAGING_URL = "https://staging-api.modrinth.com/v2";
QString MODRINTH_PROD_URL = "https://api.modrinth.com/v2";
+ QStringList MODRINTH_MRPACK_HOSTS{"cdn.modrinth.com", "github.com", "raw.githubusercontent.com", "gitlab.com"};
QString FLAME_BASE_URL = "https://api.curseforge.com/v1";
diff --git a/default.nix b/default.nix
index 146942d5..c7d0c267 100644
--- a/default.nix
+++ b/default.nix
@@ -1 +1,14 @@
-(import nix/flake-compat.nix).defaultNix
+(
+ import
+ (
+ let
+ lock = builtins.fromJSON (builtins.readFile ./flake.lock);
+ in
+ fetchTarball {
+ url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz";
+ sha256 = lock.nodes.flake-compat.locked.narHash;
+ }
+ )
+ {src = ./.;}
+)
+.defaultNix
diff --git a/flake.lock b/flake.lock
index ad9196a9..87586643 100644
--- a/flake.lock
+++ b/flake.lock
@@ -16,29 +16,31 @@
"type": "github"
}
},
- "flake-compat_2": {
- "flake": false,
+ "flake-parts": {
+ "inputs": {
+ "nixpkgs-lib": "nixpkgs-lib"
+ },
"locked": {
- "lastModified": 1673956053,
- "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
- "owner": "edolstra",
- "repo": "flake-compat",
- "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
+ "lastModified": 1683560683,
+ "narHash": "sha256-XAygPMN5Xnk/W2c1aW0jyEa6lfMDZWlQgiNtmHXytPc=",
+ "owner": "hercules-ci",
+ "repo": "flake-parts",
+ "rev": "006c75898cf814ef9497252b022e91c946ba8e17",
"type": "github"
},
"original": {
- "owner": "edolstra",
- "repo": "flake-compat",
+ "owner": "hercules-ci",
+ "repo": "flake-parts",
"type": "github"
}
},
"flake-utils": {
"locked": {
- "lastModified": 1676283394,
- "narHash": "sha256-XX2f9c3iySLCw54rJ/CZs+ZK6IQy7GXNY4nSOyu2QG4=",
+ "lastModified": 1667395993,
+ "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
"owner": "numtide",
"repo": "flake-utils",
- "rev": "3db36a8b464d0c4532ba1c7dda728f4576d6d073",
+ "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
"type": "github"
},
"original": {
@@ -86,11 +88,11 @@
},
"nixpkgs": {
"locked": {
- "lastModified": 1678693419,
- "narHash": "sha256-bbSv5yqZAW6dz+3f3f3pOUZbxpPN+3OgCljgn7P+nnQ=",
+ "lastModified": 1685012353,
+ "narHash": "sha256-U3oOge4cHnav8OLGdRVhL45xoRj4Ppd+It6nPC9nNIU=",
"owner": "nixos",
"repo": "nixpkgs",
- "rev": "8e3fad82be64c06fbfb9fd43993aec9ef4623936",
+ "rev": "aeb75dba965e790de427b73315d5addf91a54955",
"type": "github"
},
"original": {
@@ -100,40 +102,44 @@
"type": "github"
}
},
- "nixpkgs-stable": {
+ "nixpkgs-lib": {
"locked": {
- "lastModified": 1673800717,
- "narHash": "sha256-SFHraUqLSu5cC6IxTprex/nTsI81ZQAtDvlBvGDWfnA=",
+ "dir": "lib",
+ "lastModified": 1682879489,
+ "narHash": "sha256-sASwo8gBt7JDnOOstnps90K1wxmVfyhsTPPNTGBPjjg=",
"owner": "NixOS",
"repo": "nixpkgs",
- "rev": "2f9fd351ec37f5d479556cd48be4ca340da59b8f",
+ "rev": "da45bf6ec7bbcc5d1e14d3795c025199f28e0de0",
"type": "github"
},
"original": {
+ "dir": "lib",
"owner": "NixOS",
- "ref": "nixos-22.11",
+ "ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"pre-commit-hooks": {
"inputs": {
- "flake-compat": "flake-compat_2",
- "flake-utils": [
- "flake-utils"
+ "flake-compat": [
+ "flake-compat"
],
+ "flake-utils": "flake-utils",
"gitignore": "gitignore",
"nixpkgs": [
"nixpkgs"
],
- "nixpkgs-stable": "nixpkgs-stable"
+ "nixpkgs-stable": [
+ "nixpkgs"
+ ]
},
"locked": {
- "lastModified": 1678376203,
- "narHash": "sha256-3tyYGyC8h7fBwncLZy5nCUjTJPrHbmNwp47LlNLOHSM=",
+ "lastModified": 1684842236,
+ "narHash": "sha256-rYWsIXHvNhVQ15RQlBUv67W3YnM+Pd+DuXGMvCBq2IE=",
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
- "rev": "1a20b9708962096ec2481eeb2ddca29ed747770a",
+ "rev": "61e567d6497bc9556f391faebe5e410e6623217f",
"type": "github"
},
"original": {
@@ -145,7 +151,7 @@
"root": {
"inputs": {
"flake-compat": "flake-compat",
- "flake-utils": "flake-utils",
+ "flake-parts": "flake-parts",
"libnbtplusplus": "libnbtplusplus",
"nixpkgs": "nixpkgs",
"pre-commit-hooks": "pre-commit-hooks"
diff --git a/flake.nix b/flake.nix
index f656703c..c3148fe0 100644
--- a/flake.nix
+++ b/flake.nix
@@ -3,11 +3,12 @@
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
- flake-utils.url = "github:numtide/flake-utils";
+ flake-parts.url = "github:hercules-ci/flake-parts";
pre-commit-hooks = {
url = "github:cachix/pre-commit-hooks.nix";
inputs.nixpkgs.follows = "nixpkgs";
- inputs.flake-utils.follows = "flake-utils";
+ inputs.nixpkgs-stable.follows = "nixpkgs";
+ inputs.flake-compat.follows = "flake-compat";
};
flake-compat = {
url = "github:edolstra/flake-compat";
@@ -19,73 +20,8 @@
};
};
- outputs = {
- self,
- nixpkgs,
- flake-utils,
- pre-commit-hooks,
- libnbtplusplus,
- ...
- }: let
- # User-friendly version number.
- version = builtins.substring 0 8 self.lastModifiedDate;
-
- # Supported systems (qtbase is currently broken for "aarch64-darwin")
- supportedSystems = with flake-utils.lib.system; [
- x86_64-linux
- x86_64-darwin
- aarch64-linux
- ];
-
- packagesFn = pkgs: {
- prismlauncher-qt5 = pkgs.libsForQt5.callPackage ./nix {
- inherit version self libnbtplusplus;
- };
- prismlauncher = pkgs.qt6Packages.callPackage ./nix {
- inherit version self libnbtplusplus;
- };
- };
- in
- flake-utils.lib.eachSystem supportedSystems (system: let
- pkgs = nixpkgs.legacyPackages.${system};
- in {
- checks = {
- pre-commit-check = pre-commit-hooks.lib.${system}.run {
- src = ./.;
- hooks = {
- markdownlint.enable = true;
-
- alejandra.enable = true;
- deadnix.enable = true;
-
- clang-format = {
- enable =
- false; # As most of the codebase is **not** formatted, we don't want clang-format yet
- types_or = ["c" "c++"];
- };
- };
- };
- };
-
- packages = let
- packages = packagesFn pkgs;
- in
- packages // {default = packages.prismlauncher;};
-
- devShells.default = pkgs.mkShell {
- inherit (self.checks.${system}.pre-commit-check) shellHook;
- packages = with pkgs; [
- nodePackages.markdownlint-cli
- alejandra
- deadnix
- clang-tools
- ];
-
- inputsFrom = [self.packages.${system}.default];
- buildInputs = with pkgs; [ccache ninja];
- };
- })
- // {
- overlays.default = final: _: (packagesFn final);
- };
+ outputs = inputs:
+ inputs.flake-parts.lib.mkFlake
+ {inherit inputs;}
+ {imports = [./nix];};
}
diff --git a/launcher/Application.cpp b/launcher/Application.cpp
index a7c97aa7..724e6e44 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,47 @@ 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) {
+ logRulesPath = FS::PathCombine(m_rootPath, logRulesFile);
+ 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.";
}
@@ -1655,6 +1699,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/CMakeLists.txt b/launcher/CMakeLists.txt
index 6eec9b3a..9bad2a67 100644
--- a/launcher/CMakeLists.txt
+++ b/launcher/CMakeLists.txt
@@ -124,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
@@ -360,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
@@ -523,6 +527,8 @@ set(MODRINTH_SOURCES
modplatform/modrinth/ModrinthCheckUpdate.h
modplatform/modrinth/ModrinthInstanceCreationTask.cpp
modplatform/modrinth/ModrinthInstanceCreationTask.h
+ modplatform/modrinth/ModrinthPackExportTask.cpp
+ modplatform/modrinth/ModrinthPackExportTask.h
)
set(PACKWIZ_SOURCES
@@ -576,6 +582,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}"
@@ -669,6 +724,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
@@ -849,6 +908,8 @@ SET(LAUNCHER_SOURCES
ui/dialogs/EditAccountDialog.h
ui/dialogs/ExportInstanceDialog.cpp
ui/dialogs/ExportInstanceDialog.h
+ ui/dialogs/ExportMrPackDialog.cpp
+ ui/dialogs/ExportMrPackDialog.h
ui/dialogs/IconPickerDialog.cpp
ui/dialogs/IconPickerDialog.h
ui/dialogs/ImportResourceDialog.cpp
@@ -922,6 +983,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
@@ -982,6 +1045,7 @@ qt_wrap_ui(LAUNCHER_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
@@ -992,6 +1056,7 @@ qt_wrap_ui(LAUNCHER_UI
ui/dialogs/ProfileSelectDialog.ui
ui/dialogs/SkinUploadDialog.ui
ui/dialogs/ExportInstanceDialog.ui
+ ui/dialogs/ExportMrPackDialog.ui
ui/dialogs/IconPickerDialog.ui
ui/dialogs/ImportResourceDialog.ui
ui/dialogs/MSALoginDialog.ui
@@ -1155,6 +1220,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/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..a3b7d505
--- /dev/null
+++ b/launcher/FileIgnoreProxy.cpp
@@ -0,0 +1,256 @@
+// 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 "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;
+}
diff --git a/launcher/FileIgnoreProxy.h b/launcher/FileIgnoreProxy.h
new file mode 100644
index 00000000..a5a1153d
--- /dev/null
+++ b/launcher/FileIgnoreProxy.h
@@ -0,0 +1,72 @@
+// 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 <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; }
+
+ protected:
+ bool filterAcceptsColumn(int source_column, const QModelIndex& source_parent) const;
+
+ private:
+ const QString root;
+ SeparatorPrefixTree<'/'> blocked;
+};
diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp
index d98526df..835ad925 100644
--- a/launcher/FileSystem.cpp
+++ b/launcher/FileSystem.cpp
@@ -102,7 +102,7 @@ namespace fs = ghc::filesystem;
#include <linux/fs.h>
#include <sys/ioctl.h>
#include <unistd.h>
-#elif defined(Q_OS_MACOS) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
+#elif defined(Q_OS_MACOS) || defined(Q_OS_OPENBSD)
#include <sys/attr.h>
#include <sys/clonefile.h>
#elif defined(Q_OS_WIN)
@@ -1151,7 +1151,7 @@ bool clone_file(const QString& src, const QString& dst, std::error_code& ec)
return false;
}
-#elif defined(Q_OS_MACOS) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
+#elif defined(Q_OS_MACOS) || defined(Q_OS_OPENBSD)
if (!macos_bsd_clonefile(src_path, dst_path, ec)) {
qDebug() << "failed macos_bsd_clonefile:";
@@ -1380,7 +1380,7 @@ bool linux_ficlone(const std::string& src_path, const std::string& dst_path, std
return true;
}
-#elif defined(Q_OS_MACOS) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
+#elif defined(Q_OS_MACOS) || defined(Q_OS_OPENBSD)
bool macos_bsd_clonefile(const std::string& src_path, const std::string& dst_path, std::error_code& ec)
{
diff --git a/launcher/InstanceCopyTask.cpp b/launcher/InstanceCopyTask.cpp
index 4ac3b51a..57a3143a 100644
--- a/launcher/InstanceCopyTask.cpp
+++ b/launcher/InstanceCopyTask.cpp
@@ -39,7 +39,16 @@ void InstanceCopyTask::executeTask()
setStatus(tr("Copying instance %1").arg(m_origInstance->name()));
auto copySaves = [&]() {
- FS::copy savesCopy(FS::PathCombine(m_origInstance->instanceRoot(), "saves"), FS::PathCombine(m_stagingPath, "saves"));
+ 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();
@@ -123,6 +132,7 @@ void InstanceCopyTask::copyFinished()
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"));
@@ -134,6 +144,24 @@ void InstanceCopyTask::copyFinished()
}
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/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 5b33d678..b4c520cd 100644
--- a/launcher/InstanceList.cpp
+++ b/launcher/InstanceList.cpp
@@ -797,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);
}
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/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/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/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 d109d63d..e08e6fdc 100644
--- a/launcher/StringUtils.cpp
+++ b/launcher/StringUtils.cpp
@@ -33,10 +33,12 @@
* 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
@@ -113,6 +115,69 @@ int StringUtils::naturalCompare(const QString& s1, const QString& s2, Qt::CaseSe
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 36c8cf8f..f90a6ac7 100644
--- a/launcher/StringUtils.h
+++ b/launcher/StringUtils.h
@@ -37,6 +37,7 @@
#pragma once
#include <QString>
+#include <QUrl>
namespace StringUtils {
@@ -66,5 +67,16 @@ inline QString fromStdString(string s)
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..e5c66566 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))
@@ -206,10 +212,6 @@ QVariant VersionProxyModel::data(const QModelIndex &index, int role) const
return tr("Latest");
}
}
- else if(index.row() == 0)
- {
- return tr("Latest");
- }
}
}
default:
@@ -239,10 +241,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 +429,7 @@ QModelIndex VersionProxyModel::getVersion(const QString& version) const
void VersionProxyModel::clearFilters()
{
m_filters.clear();
+ m_search.clear();
filterModel->filterChanged();
}
@@ -440,11 +439,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/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/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/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 35bef05e..f8ed5214 100644
--- a/launcher/minecraft/MinecraftInstance.cpp
+++ b/launcher/minecraft/MinecraftInstance.cpp
@@ -149,9 +149,10 @@ void MinecraftInstance::loadSpecificSettings()
// special!
m_settings->registerPassthrough(global_settings->getSetting("JavaTimestamp"), javaOrLocation);
- m_settings->registerPassthrough(global_settings->getSetting("JavaVersion"), 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);
@@ -1109,79 +1110,79 @@ 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)
{
bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool();
- m_loader_mod_list.reset(new ModFolderModel(modsRoot(), shared_from_this(), is_indexed));
+ m_loader_mod_list.reset(new ModFolderModel(modsRoot(), this, is_indexed));
m_loader_mod_list->disableInteraction(isRunning());
connect(this, &BaseInstance::runningStatusChanged, m_loader_mod_list.get(), &ModFolderModel::disableInteraction);
}
return m_loader_mod_list;
}
-std::shared_ptr<ModFolderModel> MinecraftInstance::coreModList() const
+std::shared_ptr<ModFolderModel> MinecraftInstance::coreModList()
{
if (!m_core_mod_list)
{
bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool();
- m_core_mod_list.reset(new ModFolderModel(coreModsDir(), shared_from_this(), is_indexed));
+ m_core_mod_list.reset(new ModFolderModel(coreModsDir(), this, is_indexed));
m_core_mod_list->disableInteraction(isRunning());
connect(this, &BaseInstance::runningStatusChanged, m_core_mod_list.get(), &ModFolderModel::disableInteraction);
}
return m_core_mod_list;
}
-std::shared_ptr<ModFolderModel> MinecraftInstance::nilModList() const
+std::shared_ptr<ModFolderModel> MinecraftInstance::nilModList()
{
if (!m_nil_mod_list)
{
bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool();
- m_nil_mod_list.reset(new ModFolderModel(nilModsDir(), shared_from_this(), is_indexed, false));
+ m_nil_mod_list.reset(new ModFolderModel(nilModsDir(), this, is_indexed, false));
m_nil_mod_list->disableInteraction(isRunning());
connect(this, &BaseInstance::runningStatusChanged, m_nil_mod_list.get(), &ModFolderModel::disableInteraction);
}
return m_nil_mod_list;
}
-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(), shared_from_this()));
+ 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(), shared_from_this()));
+ 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(), shared_from_this()));
+ 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(), shared_from_this()));
+ 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/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/WorldList.cpp b/launcher/minecraft/WorldList.cpp
index 62e55cd4..0feee299 100644
--- a/launcher/minecraft/WorldList.cpp
+++ b/launcher/minecraft/WorldList.cpp
@@ -45,7 +45,7 @@
#include <QFileSystemWatcher>
#include <QDebug>
-WorldList::WorldList(const QString &dir, std::shared_ptr<const BaseInstance> instance)
+WorldList::WorldList(const QString &dir, BaseInstance* instance)
: QAbstractListModel(), m_instance(instance), m_dir(dir)
{
FS::ensureFolderPathExists(m_dir.absolutePath());
@@ -53,8 +53,7 @@ WorldList::WorldList(const QString &dir, std::shared_ptr<const BaseInstance> ins
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()
diff --git a/launcher/minecraft/WorldList.h b/launcher/minecraft/WorldList.h
index 10fb4e3c..96b64193 100644
--- a/launcher/minecraft/WorldList.h
+++ b/launcher/minecraft/WorldList.h
@@ -50,7 +50,7 @@ public:
IconFileRole
};
- WorldList(const QString &dir, std::shared_ptr<const BaseInstance> instance);
+ WorldList(const QString &dir, BaseInstance* instance);
virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
@@ -128,7 +128,7 @@ signals:
void changed();
protected:
- std::shared_ptr<const BaseInstance> m_instance;
+ 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..c2794147 100644
--- a/launcher/minecraft/auth/AccountList.cpp
+++ b/launcher/minecraft/auth/AccountList.cpp
@@ -333,13 +333,13 @@ QVariant AccountList::data(const QModelIndex &index, int role) const
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");
}
}
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/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..ca75cd2a 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)
diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp
index c495cd47..e613ddeb 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;
@@ -201,7 +203,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 +215,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..d4e419f4 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,15 @@ 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;
+
+ /** 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>;
@@ -85,4 +98,13 @@ public:
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 6ae25d33..af98d834 100644
--- a/launcher/minecraft/mod/ModFolderModel.cpp
+++ b/launcher/minecraft/mod/ModFolderModel.cpp
@@ -37,6 +37,7 @@
#include "ModFolderModel.h"
#include <FileSystem.h>
+#include <qheaderview.h>
#include <QDebug>
#include <QFileSystemWatcher>
#include <QIcon>
@@ -52,12 +53,14 @@
#include "minecraft/mod/tasks/LocalModParseTask.h"
#include "minecraft/mod/tasks/ModFolderLoadTask.h"
-#include "modplatform/ModIndex.h"
-ModFolderModel::ModFolderModel(const QString& dir, std::shared_ptr<const BaseInstance> instance, bool is_indexed, bool create_dir)
+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
@@ -118,7 +121,9 @@ QVariant ModFolderModel::data(const QModelIndex &index, int role) const
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:
@@ -142,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();
}
diff --git a/launcher/minecraft/mod/ModFolderModel.h b/launcher/minecraft/mod/ModFolderModel.h
index 46f5087f..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, std::shared_ptr<const BaseInstance> instance, 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/ResourceFolderModel.cpp b/launcher/minecraft/mod/ResourceFolderModel.cpp
index e1973468..7700fd36 100644
--- a/launcher/minecraft/mod/ResourceFolderModel.cpp
+++ b/launcher/minecraft/mod/ResourceFolderModel.cpp
@@ -8,15 +8,18 @@
#include <QStyle>
#include <QThreadPool>
#include <QUrl>
+#include <QMenu>
#include "Application.h"
#include "FileSystem.h"
+#include "QVariantUtils.h"
#include "minecraft/mod/tasks/BasicFolderLoadTask.h"
+#include "settings/Setting.h"
#include "tasks/Task.h"
-ResourceFolderModel::ResourceFolderModel(QDir dir, std::shared_ptr<const BaseInstance> instance, QObject* parent, bool create_dir)
+ResourceFolderModel::ResourceFolderModel(QDir dir, BaseInstance* instance, QObject* parent, bool create_dir)
: QAbstractListModel(parent), m_dir(dir), m_instance(instance), m_watcher(this)
{
if (create_dir) {
@@ -471,10 +474,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 {};
}
@@ -500,6 +503,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);
diff --git a/launcher/minecraft/mod/ResourceFolderModel.h b/launcher/minecraft/mod/ResourceFolderModel.h
index fdf5f331..eb1d7c4f 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>
@@ -26,9 +29,11 @@ class QSortFilterProxyModel;
class ResourceFolderModel : public QAbstractListModel {
Q_OBJECT
public:
- ResourceFolderModel(QDir, std::shared_ptr<const BaseInstance>, 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.
@@ -92,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; };
@@ -110,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!
@@ -117,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:
@@ -187,11 +199,14 @@ class ResourceFolderModel : public QAbstractListModel {
// 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 };
+ 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 };
bool m_can_interact = true;
QDir m_dir;
- std::shared_ptr<const BaseInstance> m_instance;
+ 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..e06c1ac1 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);
}
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 6eba4e2e..41455599 100644
--- a/launcher/minecraft/mod/ResourcePackFolderModel.cpp
+++ b/launcher/minecraft/mod/ResourcePackFolderModel.cpp
@@ -35,6 +35,8 @@
*/
#include "ResourcePackFolderModel.h"
+#include <qnamespace.h>
+#include <qsize.h>
#include <QIcon>
#include <QStyle>
@@ -45,10 +47,14 @@
#include "minecraft/mod/tasks/BasicFolderLoadTask.h"
#include "minecraft/mod/tasks/LocalResourcePackParseTask.h"
-ResourcePackFolderModel::ResourcePackFolderModel(const QString& dir, std::shared_ptr<const BaseInstance> instance)
+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
@@ -84,9 +90,11 @@ QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const
return {};
}
case Qt::DecorationRole: {
- if (column == NAME_COLUMN && (at(row)->isSymLinkUnder(instDirPath()) || at(row)->isMoreThanOneHardLink()))
+ 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: {
@@ -94,7 +102,7 @@ QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const
//: 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 == NAME_COLUMN) {
+ 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."
@@ -126,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 {};
}
@@ -151,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 66d5a074..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, std::shared_ptr<const BaseInstance> instance);
+ 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 6f3f2811..f8249962 100644
--- a/launcher/minecraft/mod/ShaderPackFolderModel.h
+++ b/launcher/minecraft/mod/ShaderPackFolderModel.h
@@ -6,7 +6,9 @@ class ShaderPackFolderModel : public ResourceFolderModel {
Q_OBJECT
public:
- explicit ShaderPackFolderModel(const QString& dir, std::shared_ptr<const BaseInstance> instance)
+ 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 1e218537..531a7023 100644
--- a/launcher/minecraft/mod/TexturePackFolderModel.cpp
+++ b/launcher/minecraft/mod/TexturePackFolderModel.cpp
@@ -33,15 +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, std::shared_ptr<const BaseInstance> instance)
+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()
{
@@ -52,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 246997bd..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:
+
+ 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 5342d693..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,6 +318,58 @@ 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;
}
@@ -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/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/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..93b5ce76 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);
}
}
@@ -212,12 +213,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 +265,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 +278,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 +346,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 +414,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 +422,7 @@ Task::Ptr EnsureMetadataTask::flameProjectsTask()
}
}
- auto response = new QByteArray();
+ auto response = std::make_shared<QByteArray>();
Task::Ptr proj_task;
if (addonIds.isEmpty()) {
@@ -434,7 +435,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.h b/launcher/modplatform/ModIndex.h
index 40f1efc4..3b0a03a1 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,28 @@ 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; });
}
};
+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 } };
+};
+
} // 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..ce7a6055 100644
--- a/launcher/modplatform/flame/FileResolvingTask.cpp
+++ b/launcher/modplatform/flame/FileResolvingTask.cpp
@@ -25,17 +25,40 @@ void Flame::FileResolvingTask::executeTask()
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 +67,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,24 +88,42 @@ 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();
}
@@ -95,7 +136,6 @@ void Flame::FileResolvingTask::modrinthCheckFinished() {
auto &out = *it;
auto bytes = blockedProjects[out];
if (!out->resolved) {
- delete bytes;
continue;
}
@@ -112,11 +152,9 @@ 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 *>();
+ 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;
@@ -124,32 +162,48 @@ void Flame::FileResolvingTask::modrinthCheckFinished() {
//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();
+ 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 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 mod = block->at(index);
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..0a6dc78f 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,9 +17,9 @@ 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;
@@ -41,14 +44,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 +77,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..f003ada9 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) {
@@ -496,7 +501,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..."));
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/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/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..36c142ac 100644
--- a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp
+++ b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp
@@ -81,6 +81,7 @@ void PackInstallTask::downloadPack()
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::stepProgress, this, &PackInstallTask::propogateStepProgress);
connect(netJobContainer.get(), &NetJob::aborted, this, &PackInstallTask::onDownloadAborted);
netJobContainer->start();
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..e83ed2bf 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 }) {
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(',');
@@ -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);
- }
+ inline auto validateModLoaders(ModLoaderTypes loaders) const -> bool { return loaders & (Forge | Fabric | Quilt); }
+ [[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..4cd88aa6
--- /dev/null
+++ b/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp
@@ -0,0 +1,319 @@
+// 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()
+{
+ 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 {
+ 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 obj;
+ obj["formatVersion"] = 1;
+ obj["game"] = "minecraft";
+ obj["name"] = name;
+ obj["versionId"] = version;
+ if (!summary.isEmpty())
+ obj["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;
+
+ obj["dependencies"] = dependencies;
+ }
+
+ QJsonArray files;
+ QMapIterator<QString, ResolvedFile> iterator(resolvedFiles);
+ while (iterator.hasNext()) {
+ iterator.next();
+
+ const ResolvedFile& value = iterator.value();
+
+ QJsonObject file;
+ file["path"] = iterator.key();
+ file["downloads"] = QJsonArray({ iterator.value().url });
+
+ QJsonObject hashes;
+ hashes["sha1"] = value.sha1;
+ hashes["sha512"] = value.sha512;
+
+ file["hashes"] = hashes;
+ file["fileSize"] = value.size;
+
+ files << file;
+ }
+ obj["files"] = files;
+
+ return QJsonDocument(obj).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/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/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 f0347cab..d16256b9 100644
--- a/launcher/settings/INIFile.cpp
+++ b/launcher/settings/INIFile.cpp
@@ -45,12 +45,12 @@
#include <QSettings>
-INIFile::INIFile()
-{
-}
+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);
@@ -71,6 +71,81 @@ bool INIFile::saveFile(QString fileName)
return true;
}
+QString unescape(QString orig)
+{
+ QString out;
+ QChar prev = QChar::Null;
+ for (auto c : orig) {
+ if (prev == '\\') {
+ if (c == 'n')
+ out += '\n';
+ else if (c == 't')
+ out += '\t';
+ else if (c == '#')
+ out += '#';
+ else
+ out += c;
+ prev = QChar::Null;
+ } else {
+ if (c == '\\') {
+ prev = c;
+ continue;
+ }
+ out += c;
+ prev = QChar::Null;
+ }
+ }
+ return out;
+}
+
+QString unquote(QString str)
+{
+ 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 str;
+}
+
+bool parseOldFileFormat(QIODevice& device, QSettings::SettingsMap& map)
+{
+ 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];
+ // 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) == '\\') {
+ continue;
+ }
+ line = line.left(lineRaw.indexOf('#')).trimmed();
+ }
+
+ int eqPos = line.indexOf('=');
+ if (eqPos == -1)
+ continue;
+ QString key = line.left(eqPos).trimmed();
+ QString valueStr = line.right(line.length() - eqPos - 1).trimmed();
+
+ valueStr = unquote(unescape(valueStr));
+
+ QVariant value(valueStr);
+ map.insert(key, value);
+ }
+
+ return true;
+}
bool INIFile::loadFile(QString fileName)
{
@@ -84,10 +159,29 @@ bool INIFile::loadFile(QString fileName)
qCritical() << "A format error occurred (e.g. loading a malformed INI file).";
return false;
}
-
- for (auto&& key : _settings_obj.allKeys())
- insert(key, _settings_obj.value(key));
-
+ 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;
}
@@ -103,4 +197,3 @@ void INIFile::set(QString key, QVariant val)
{
this->operator[](key) = val;
}
-
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/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..23e55c51 100644
--- a/launcher/translations/TranslationsModel.cpp
+++ b/launcher/translations/TranslationsModel.cpp
@@ -190,7 +190,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;
@@ -673,8 +673,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/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp
index 7e2c4acf..eeb78c53 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
@@ -107,6 +107,7 @@
#include "ui/dialogs/CopyInstanceDialog.h"
#include "ui/dialogs/EditAccountDialog.h"
#include "ui/dialogs/ExportInstanceDialog.h"
+#include "ui/dialogs/ExportMrPackDialog.h"
#include "ui/dialogs/ImportResourceDialog.h"
#include "ui/themes/ITheme.h"
#include "ui/themes/ThemeManager.h"
@@ -186,7 +187,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi
}
- // 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 +200,12 @@ 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);
+ ui->actionExportInstance->setMenu(exportInstanceMenu);
}
// hide, disable and show stuff
@@ -421,15 +426,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());
@@ -439,6 +435,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());
@@ -478,7 +480,23 @@ void MainWindow::lockToolbars(bool 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)
@@ -680,6 +698,15 @@ 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();
@@ -694,6 +721,8 @@ void MainWindow::repopulateAccountsMenu()
}
}
+ QActionGroup* accountsGroup = new QActionGroup(this);
+
if (accounts->count() <= 0)
{
ui->actionNoAccountsAdded->setEnabled(false);
@@ -709,6 +738,7 @@ void MainWindow::repopulateAccountsMenu()
QAction *action = new QAction(profileLabel, this);
action->setData(i);
action->setCheckable(true);
+ action->setActionGroup(accountsGroup);
if (defaultAccount == account)
{
action->setChecked(true);
@@ -737,6 +767,7 @@ void MainWindow::repopulateAccountsMenu()
ui->actionNoDefaultAccount->setData(-1);
ui->actionNoDefaultAccount->setChecked(!defaultAccount);
+ ui->actionNoDefaultAccount->setActionGroup(accountsGroup);
ui->accountsMenu->addAction(ui->actionNoDefaultAccount);
@@ -744,6 +775,8 @@ void MainWindow::repopulateAccountsMenu()
ui->accountsMenu->addSeparator();
ui->accountsMenu->addAction(ui->actionManageAccounts);
+
+ accountsButtonMenu->addActions(ui->accountsMenu->actions());
}
void MainWindow::updatesAllowedChanged(bool allowed)
@@ -1208,6 +1241,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();
@@ -1366,7 +1405,7 @@ void MainWindow::on_actionDeleteInstance_triggered()
APPLICATION->instances()->deleteInstance(id);
}
-void MainWindow::on_actionExportInstance_triggered()
+void MainWindow::on_actionExportInstanceZip_triggered()
{
if (m_selectedInstance)
{
@@ -1375,6 +1414,15 @@ void MainWindow::on_actionExportInstance_triggered()
}
}
+void MainWindow::on_actionExportInstanceMrPack_triggered()
+{
+ if (m_selectedInstance)
+ {
+ ExportMrPackDialog dlg(m_selectedInstance, this);
+ dlg.exec();
+ }
+}
+
void MainWindow::on_actionRenameInstance_triggered()
{
if (m_selectedInstance)
diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h
index 3a42c34e..3bb20c4a 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,9 @@ 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_actionRenameInstance_triggered();
diff --git a/launcher/ui/MainWindow.ui b/launcher/ui/MainWindow.ui
index 2b6a10b1..113dfc1e 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,23 @@
<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="actionCreateInstanceShortcut">
@@ -528,6 +542,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 +577,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/ExportInstanceDialog.cpp b/launcher/ui/dialogs/ExportInstanceDialog.cpp
index 07ec3c70..8ecd91a9 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
@@ -46,301 +47,21 @@
#include <QSaveFile>
#include <QStack>
#include <QFileInfo>
-
-#include "StringUtils.h"
#include "SeparatorPrefixTree.h"
#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;
-};
-
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);
+ model->setIconProvider(&icons);
+ auto root = instance->instanceRoot();
+ proxyModel = new FileIgnoreProxy(root, this);
loadPackIgnore();
proxyModel->setSourceModel(model);
- auto root = instance->instanceRoot();
ui->treeView->setModel(proxyModel);
ui->treeView->setRootIndex(proxyModel->mapFromSource(model->index(root)));
ui->treeView->sortByColumn(0, Qt::AscendingOrder);
@@ -404,22 +125,11 @@ 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);
@@ -511,5 +221,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/ExportMrPackDialog.cpp b/launcher/ui/dialogs/ExportMrPackDialog.cpp
new file mode 100644
index 00000000..60ecefd5
--- /dev/null
+++ b/launcher/ui/dialogs/ExportMrPackDialog.cpp
@@ -0,0 +1,124 @@
+// 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 "ExportMrPackDialog.h"
+#include "minecraft/mod/ModFolderModel.h"
+#include "ui/dialogs/CustomMessageBox.h"
+#include "ui/dialogs/ProgressDialog.h"
+#include "ui_ExportMrPackDialog.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"
+
+ExportMrPackDialog::ExportMrPackDialog(InstancePtr instance, QWidget* parent)
+ : QDialog(parent), instance(instance), ui(new Ui::ExportMrPackDialog)
+{
+ ui->setupUi(this);
+ ui->name->setText(instance->name());
+ ui->summary->setText(instance->notes().split(QRegularExpression("\\r?\\n"))[0]);
+
+ // ensure a valid pack is generated
+ // the name and version fields mustn't be empty
+ connect(ui->name, &QLineEdit::textEdited, this, &ExportMrPackDialog::validate);
+ connect(ui->version, &QLineEdit::textEdited, this, &ExportMrPackDialog::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->setSourceModel(model);
+ proxy->setFilterRegularExpression("^(?!(\\.DS_Store)|([tT]humbs\\.db)).+$");
+
+ 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) {
+ 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);
+}
+
+ExportMrPackDialog::~ExportMrPackDialog()
+{
+ delete ui;
+}
+
+void ExportMrPackDialog::done(int result)
+{
+ if (result == Accepted) {
+ const QString filename = FS::RemoveInvalidFilenameChars(ui->name->text());
+ const QString output = QFileDialog::getSaveFileName(this, tr("Export %1").arg(ui->name->text()),
+ FS::PathCombine(QDir::homePath(), filename + ".mrpack"),
+ "Modrinth pack (*.mrpack *.zip)", nullptr);
+
+ if (output.isEmpty())
+ return;
+
+ ModrinthPackExportTask task(ui->name->text(), ui->version->text(), ui->summary->text(), instance, output,
+ [this](const QString& path) { return proxy->blockedPaths().covers(path); });
+
+ connect(&task, &Task::failed,
+ [this](const QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); });
+ connect(&task, &Task::aborted, [this] {
+ CustomMessageBox::selectable(this, tr("Task aborted"), tr("The task has been aborted by the user."), QMessageBox::Information)
+ ->show();
+ });
+
+ ProgressDialog progress(this);
+ progress.setSkipButton(true, tr("Abort"));
+ if (progress.execWithTask(&task) != QDialog::Accepted)
+ return;
+ }
+
+ QDialog::done(result);
+}
+
+void ExportMrPackDialog::validate()
+{
+ const bool invalid = ui->name->text().isEmpty() || ui->version->text().isEmpty();
+ ui->buttonBox->button(QDialogButtonBox::Ok)->setDisabled(invalid);
+}
diff --git a/launcher/ui/dialogs/ExportMrPackDialog.h b/launcher/ui/dialogs/ExportMrPackDialog.h
new file mode 100644
index 00000000..1c70c4ae
--- /dev/null
+++ b/launcher/ui/dialogs/ExportMrPackDialog.h
@@ -0,0 +1,45 @@
+// 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"
+
+namespace Ui {
+class ExportMrPackDialog;
+}
+
+class ExportMrPackDialog : public QDialog {
+ Q_OBJECT
+
+ public:
+ explicit ExportMrPackDialog(InstancePtr instance, QWidget* parent = nullptr);
+ ~ExportMrPackDialog();
+
+ void done(int result) override;
+ void validate();
+
+ private:
+ const InstancePtr instance;
+ Ui::ExportMrPackDialog* ui;
+ FileIgnoreProxy* proxy;
+ FastFileIconProvider icons;
+};
diff --git a/launcher/ui/dialogs/ExportMrPackDialog.ui b/launcher/ui/dialogs/ExportMrPackDialog.ui
new file mode 100644
index 00000000..9a789737
--- /dev/null
+++ b/launcher/ui/dialogs/ExportMrPackDialog.ui
@@ -0,0 +1,136 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ExportMrPackDialog</class>
+ <widget class="QDialog" name="ExportMrPackDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>650</width>
+ <height>413</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Export Modrinth 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="versionLabel">
+ <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="summaryLabel">
+ <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>ExportMrPackDialog</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>ExportMrPackDialog</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 64ed7673..aafaf220 100644
--- a/launcher/ui/dialogs/NewInstanceDialog.cpp
+++ b/launcher/ui/dialogs/NewInstanceDialog.cpp
@@ -99,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);
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..246a0fd4 100644
--- a/launcher/ui/dialogs/ProgressDialog.cpp
+++ b/launcher/ui/dialogs/ProgressDialog.cpp
@@ -1,29 +1,69 @@
-/* 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 "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);
@@ -54,10 +94,24 @@ ProgressDialog::~ProgressDialog()
}
void ProgressDialog::updateSize()
-{
+{
+ QSize lastSize = this->size();
QSize qSize = QSize(480, minimumSizeHint().height());
- resize(qSize);
- setFixedSize(qSize);
+
+ // if the current window is too small
+ if ((lastSize != qSize) && (lastSize.height() < qSize.height()))
+ {
+ resize(qSize);
+
+ // keep the dialog in the center after a resize
+ this->move(
+ this->parentWidget()->x() + (this->parentWidget()->width() - this->width()) / 2,
+ this->parentWidget()->y() + (this->parentWidget()->height() - this->height()) / 2
+ );
+ }
+
+ setMinimumSize(qSize);
+
}
int ProgressDialog::execWithTask(Task* task)
@@ -79,17 +133,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 +201,53 @@ void ProgressDialog::onTaskSucceeded()
void ProgressDialog::changeStatus(const QString& status)
{
ui->globalStatusLabel->setText(task->getStatus());
- ui->statusLabel->setText(task->getStepStatus());
+ ui->globalStatusDetailsLabel->setText(task->getDetails());
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..fc9a0fbc 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;
@@ -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..4f59f560 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"
@@ -41,7 +48,10 @@
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 +99,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 +112,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 +123,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 +212,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 +265,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)
{
@@ -232,6 +290,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 +325,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 +352,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 +377,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/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..dca1b3a6 100644
--- a/launcher/ui/pages/global/APIPage.cpp
+++ b/launcher/ui/pages/global/APIPage.cpp
@@ -177,7 +177,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..d9116bfc 100644
--- a/launcher/ui/pages/global/LauncherPage.ui
+++ b/launcher/ui/pages/global/LauncherPage.ui
@@ -172,7 +172,7 @@
<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.ui b/launcher/ui/pages/global/MinecraftPage.ui
index 103881b5..8f5de725 100644
--- a/launcher/ui/pages/global/MinecraftPage.ui
+++ b/launcher/ui/pages/global/MinecraftPage.ui
@@ -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>
diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.cpp b/launcher/ui/pages/instance/ExternalResourcesPage.cpp
index 1115ddc3..173bcb66 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();
@@ -96,7 +155,6 @@ void ExternalResourcesPage::itemActivated(const QModelIndex&)
return;
auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection());
- m_model->setResourceEnabled(selection.indexes(), EnableAction::TOGGLE);
}
void ExternalResourcesPage::filterTextChanged(const QString& newContents)
@@ -248,6 +306,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..6c0a12cb 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;
diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.ui b/launcher/ui/pages/instance/ExternalResourcesPage.ui
index 33a03336..f676361c 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>
diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.cpp b/launcher/ui/pages/instance/InstanceSettingsPage.cpp
index 4b4c73dc..08977841 100644
--- a/launcher/ui/pages/instance/InstanceSettingsPage.cpp
+++ b/launcher/ui/pages/instance/InstanceSettingsPage.cpp
@@ -60,21 +60,18 @@ 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);
+ // As the signal will (probably) not be triggered once we click edit, let's update it manually instead.
+ updateRunningStatus(m_instance->isRunning());
+ connect(m_instance, &BaseInstance::runningStatusChanged, this, &InstanceSettingsPage::updateRunningStatus);
connect(ui->openGlobalJavaSettingsButton, &QCommandLinkButton::clicked, this, &InstanceSettingsPage::globalSettingsButtonClicked);
connect(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()
@@ -454,36 +451,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 +473,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)
@@ -552,3 +523,8 @@ void InstanceSettingsPage::updateThresholds()
ui->labelMaxMemIcon->setPixmap(pix);
}
}
+
+void InstanceSettingsPage::updateRunningStatus(bool running)
+{
+ setEnabled(!running);
+}
diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.h b/launcher/ui/pages/instance/InstanceSettingsPage.h
index cb6fbae0..0438fe3b 100644
--- a/launcher/ui/pages/instance/InstanceSettingsPage.h
+++ b/launcher/ui/pages/instance/InstanceSettingsPage.h
@@ -75,12 +75,12 @@ public:
{
return "Instance-settings";
}
- virtual bool shouldDisplay() const override;
void retranslate() override;
void updateThresholds();
private slots:
+ void updateRunningStatus(bool running);
void on_javaDetectBtn_clicked();
void on_javaTestBtn_clicked();
void on_javaBrowseBtn_clicked();
@@ -95,12 +95,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..e0a7314f 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,37 @@ 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,8 +91,10 @@ 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){
@@ -195,7 +226,7 @@ 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{};
@@ -338,7 +369,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{};
diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp
index 4548af59..90e7d0d6 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
@@ -86,28 +87,20 @@ ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr<ModFolderModel>
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());
+ return (!m_instance || !m_instance->isRunning()) && (ui->treeView->selectionModel()->hasSelection() || !m_model->empty());
};
- connect(ui->treeView->selectionModel(), &QItemSelectionModel::selectionChanged, this, [this, check_allow_update] {
- ui->actionUpdateItem->setEnabled(check_allow_update());
- });
+ connect(ui->treeView->selectionModel(), &QItemSelectionModel::selectionChanged, this,
+ [this, check_allow_update] { ui->actionUpdateItem->setEnabled(check_allow_update()); });
- connect(mods.get(), &ModFolderModel::rowsInserted, this, [this, check_allow_update] {
- ui->actionUpdateItem->setEnabled(check_allow_update());
- });
+ connect(mods.get(), &ModFolderModel::rowsInserted, 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());
- });
+ connect(mods.get(), &ModFolderModel::rowsRemoved, this,
+ [this, check_allow_update] { ui->actionUpdateItem->setEnabled(check_allow_update()); });
- connect(mods.get(), &ModFolderModel::updateFinished, this, [this, check_allow_update, mods] {
- 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::updateFinished, 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());
diff --git a/launcher/ui/pages/instance/VersionPage.cpp b/launcher/ui/pages/instance/VersionPage.cpp
index 7fff3b93..74b7ec7c 100644
--- a/launcher/ui/pages/instance/VersionPage.cpp
+++ b/launcher/ui/pages/instance/VersionPage.cpp
@@ -165,7 +165,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);
@@ -501,7 +501,7 @@ void VersionPage::on_actionDownload_All_triggered()
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();
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..aab2ee89 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()
@@ -156,31 +159,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 +195,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 +244,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 +262,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 +288,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 +297,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 +317,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 +345,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 +361,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 +391,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/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/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/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..9c041bfe 100644
--- a/launcher/ui/widgets/InfoFrame.cpp
+++ b/launcher/ui/widgets/InfoFrame.cpp
@@ -47,6 +47,8 @@ InfoFrame::InfoFrame(QWidget *parent) :
ui->setupUi(this);
ui->descriptionLabel->setHidden(true);
ui->nameLabel->setHidden(true);
+ ui->licenseLabel->setHidden(true);
+ ui->issueTrackerLabel->setHidden(true);
updateHiddenState();
}
@@ -88,7 +90,41 @@ void InfoFrame::updateWithMod(Mod const& m)
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)
@@ -177,16 +213,16 @@ 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);
}
}
@@ -251,6 +287,66 @@ void InfoFrame::setDescription(QString text)
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()) {
@@ -275,6 +371,20 @@ void InfoFrame::descriptionEllipsisHandler(QString link)
}
}
+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..7eb679a9 100644
--- a/launcher/ui/widgets/InfoFrame.h
+++ b/launcher/ui/widgets/InfoFrame.h
@@ -36,6 +36,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 +50,7 @@ class InfoFrame : public QFrame {
public slots:
void descriptionEllipsisHandler(QString link);
+ void licenseEllipsisHandler(QString link);
void boxClosed(int result);
private:
@@ -56,5 +59,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/ModListView.cpp b/launcher/ui/widgets/ModListView.cpp
index 09b03a76..893cd120 100644
--- a/launcher/ui/widgets/ModListView.cpp
+++ b/launcher/ui/widgets/ModListView.cpp
@@ -79,3 +79,12 @@ void ModListView::setModel ( QAbstractItemModel* model )
});
}
}
+
+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/libraries/katabasis/src/Reply.cpp b/libraries/katabasis/src/Reply.cpp
index 3e27a7e6..c2607900 100644
--- a/libraries/katabasis/src/Reply.cpp
+++ b/libraries/katabasis/src/Reply.cpp
@@ -40,6 +40,8 @@ void ReplyList::remove(QNetworkReply *reply) {
if (o2Reply) {
o2Reply->stop();
(void)replies_.removeOne(o2Reply);
+ // we took ownership, we must free
+ delete o2Reply;
}
}
diff --git a/nix/default.nix b/nix/default.nix
index e0616b6e..7bad1440 100644
--- a/nix/default.nix
+++ b/nix/default.nix
@@ -1,100 +1,32 @@
{
- lib,
- stdenv,
- cmake,
- ninja,
- jdk8,
- jdk17,
- zlib,
- file,
- wrapQtAppsHook,
- xorg,
- libpulseaudio,
- qtbase,
- qtsvg,
- qtwayland,
- libGL,
- quazip,
- glfw,
- openal,
- extra-cmake-modules,
- tomlplusplus,
- ghc_filesystem,
- cmark,
- msaClientID ? "",
- jdks ? [jdk17 jdk8],
- gamemodeSupport ? true,
- gamemode,
- # flake
+ inputs,
self,
- version,
- libnbtplusplus,
-}:
-stdenv.mkDerivation rec {
- pname = "prismlauncher";
- inherit version;
-
- src = lib.cleanSource self;
-
- nativeBuildInputs = [extra-cmake-modules cmake file jdk17 ninja wrapQtAppsHook];
- buildInputs =
- [
- qtbase
- qtsvg
- zlib
- quazip
- ghc_filesystem
- tomlplusplus
- cmark
- ]
- ++ lib.optional (lib.versionAtLeast qtbase.version "6") qtwayland
- ++ lib.optional gamemodeSupport gamemode.dev;
+ ...
+}: {
+ imports = [
+ ./dev.nix
+ ./distribution.nix
+ ];
- cmakeFlags =
- lib.optionals (msaClientID != "") ["-DLauncher_MSA_CLIENT_ID=${msaClientID}"]
- ++ lib.optionals (lib.versionOlder qtbase.version "6") ["-DLauncher_QT_VERSION_MAJOR=5"];
+ _module.args = {
+ # User-friendly version number.
+ version = builtins.substring 0 8 self.lastModifiedDate;
+ };
- postUnpack = ''
- rm -rf source/libraries/libnbtplusplus
- mkdir source/libraries/libnbtplusplus
- ln -s ${libnbtplusplus}/* source/libraries/libnbtplusplus
- chmod -R +r+w source/libraries/libnbtplusplus
- chown -R $USER: source/libraries/libnbtplusplus
- '';
+ perSystem = {system, ...}: {
+ # Nixpkgs instantiated for supported systems with our overlay.
+ _module.args.pkgs = import inputs.nixpkgs {
+ inherit system;
+ overlays = [self.overlays.default];
+ };
+ };
- qtWrapperArgs = let
- libpath = with xorg;
- lib.makeLibraryPath ([
- libX11
- libXext
- libXcursor
- libXrandr
- libXxf86vm
- libpulseaudio
- libGL
- glfw
- openal
- stdenv.cc.cc.lib
- ]
- ++ lib.optional gamemodeSupport gamemode.lib);
- in [
- "--set LD_LIBRARY_PATH /run/opengl-driver/lib:${libpath}"
- "--prefix PRISMLAUNCHER_JAVA_PATHS : ${lib.makeSearchPath "bin/java" jdks}"
- # xorg.xrandr needed for LWJGL [2.9.2, 3) https://github.com/LWJGL/lwjgl/issues/128
- "--prefix PATH : ${lib.makeBinPath [xorg.xrandr]}"
+ # Supported systems.
+ systems = [
+ "x86_64-linux"
+ "x86_64-darwin"
+ "aarch64-linux"
+ # Disabled due to qtbase being currently broken for "aarch64-darwin."
+ # "aarch64-darwin"
];
-
- meta = with lib; {
- homepage = "https://prismlauncher.org/";
- description = "A free, open source launcher for Minecraft";
- longDescription = ''
- Allows you to have multiple, separate instances of Minecraft (each with
- their own mods, texture packs, saves, etc) and helps you manage them and
- their associated options with a simple interface.
- '';
- platforms = platforms.linux;
- changelog = "https://github.com/PrismLauncher/PrismLauncher/releases/tag/${version}";
- license = licenses.gpl3Only;
- maintainers = with maintainers; [minion3665 Scrumplex];
- };
}
diff --git a/nix/dev.nix b/nix/dev.nix
new file mode 100644
index 00000000..635c6bb4
--- /dev/null
+++ b/nix/dev.nix
@@ -0,0 +1,46 @@
+{
+ inputs,
+ self,
+ ...
+}: {
+ perSystem = {
+ system,
+ pkgs,
+ ...
+ }: {
+ checks = {
+ pre-commit-check = inputs.pre-commit-hooks.lib.${system}.run {
+ src = self;
+ hooks = {
+ markdownlint.enable = true;
+
+ alejandra.enable = true;
+ deadnix.enable = true;
+ nil.enable = true;
+
+ clang-format = {
+ enable =
+ false; # As most of the codebase is **not** formatted, we don't want clang-format yet
+ types_or = ["c" "c++"];
+ };
+ };
+ };
+ };
+
+ devShells.default = pkgs.mkShell {
+ inherit (self.checks.${system}.pre-commit-check) shellHook;
+ packages = with pkgs; [
+ nodePackages.markdownlint-cli
+ alejandra
+ deadnix
+ clang-tools
+ nil
+ ];
+
+ inputsFrom = [self.packages.${system}.prismlauncher-unwrapped];
+ buildInputs = with pkgs; [ccache ninja];
+ };
+
+ formatter = pkgs.alejandra;
+ };
+}
diff --git a/nix/distribution.nix b/nix/distribution.nix
new file mode 100644
index 00000000..0f2e26f3
--- /dev/null
+++ b/nix/distribution.nix
@@ -0,0 +1,29 @@
+{
+ inputs,
+ self,
+ version,
+ ...
+}: {
+ perSystem = {pkgs, ...}: {
+ packages = {
+ inherit (pkgs) prismlauncher-qt5-unwrapped prismlauncher-qt5 prismlauncher-unwrapped prismlauncher;
+ default = pkgs.prismlauncher;
+ };
+ };
+
+ flake = {
+ overlays.default = final: prev: let
+ # Helper function to build prism against different versions of Qt.
+ mkPrism = qt:
+ qt.callPackage ./package.nix {
+ inherit (inputs) libnbtplusplus;
+ inherit self version;
+ };
+ in {
+ prismlauncher-qt5-unwrapped = mkPrism final.libsForQt5;
+ prismlauncher-qt5 = prev.prismlauncher-qt5.override {prismlauncher-unwrapped = final.prismlauncher-qt5-unwrapped;};
+ prismlauncher-unwrapped = mkPrism final.qt6Packages;
+ prismlauncher = prev.prismlauncher.override {inherit (final) prismlauncher-unwrapped;};
+ };
+ };
+}
diff --git a/nix/flake-compat.nix b/nix/flake-compat.nix
deleted file mode 100644
index 7162a6cf..00000000
--- a/nix/flake-compat.nix
+++ /dev/null
@@ -1,9 +0,0 @@
-let
- lock = builtins.fromJSON (builtins.readFile ../flake.lock);
- inherit (lock.nodes.flake-compat.locked) rev narHash;
- flake-compat = fetchTarball {
- url = "https://github.com/edolstra/flake-compat/archive/${rev}.tar.gz";
- sha256 = narHash;
- };
-in
- import flake-compat {src = ../.;}
diff --git a/nix/package.nix b/nix/package.nix
new file mode 100644
index 00000000..edc266dc
--- /dev/null
+++ b/nix/package.nix
@@ -0,0 +1,65 @@
+{
+ lib,
+ stdenv,
+ cmake,
+ ninja,
+ jdk17,
+ zlib,
+ qtbase,
+ quazip,
+ extra-cmake-modules,
+ tomlplusplus,
+ cmark,
+ ghc_filesystem,
+ gamemode,
+ msaClientID ? null,
+ gamemodeSupport ? true,
+ self,
+ version,
+ libnbtplusplus,
+}:
+stdenv.mkDerivation rec {
+ pname = "prismlauncher-unwrapped";
+ inherit version;
+
+ src = lib.cleanSource self;
+
+ nativeBuildInputs = [extra-cmake-modules cmake jdk17 ninja];
+ buildInputs =
+ [
+ qtbase
+ zlib
+ quazip
+ ghc_filesystem
+ tomlplusplus
+ cmark
+ ]
+ ++ lib.optional gamemodeSupport gamemode;
+
+ hardeningEnable = ["pie"];
+
+ cmakeFlags =
+ lib.optionals (msaClientID != null) ["-DLauncher_MSA_CLIENT_ID=${msaClientID}"]
+ ++ lib.optionals (lib.versionOlder qtbase.version "6") ["-DLauncher_QT_VERSION_MAJOR=5"];
+
+ postUnpack = ''
+ rm -rf source/libraries/libnbtplusplus
+ ln -s ${libnbtplusplus} source/libraries/libnbtplusplus
+ '';
+
+ dontWrapQtApps = true;
+
+ meta = with lib; {
+ homepage = "https://prismlauncher.org/";
+ description = "A free, open source launcher for Minecraft";
+ longDescription = ''
+ Allows you to have multiple, separate instances of Minecraft (each with
+ their own mods, texture packs, saves, etc) and helps you manage them and
+ their associated options with a simple interface.
+ '';
+ platforms = platforms.linux;
+ changelog = "https://github.com/PrismLauncher/PrismLauncher/releases/tag/${version}";
+ license = licenses.gpl3Only;
+ maintainers = with maintainers; [minion3665 Scrumplex];
+ };
+}
diff --git a/program_info/org.prismlauncher.PrismLauncher.desktop.in b/program_info/org.prismlauncher.PrismLauncher.desktop.in
index f08f2ba4..20fabe9d 100644
--- a/program_info/org.prismlauncher.PrismLauncher.desktop.in
+++ b/program_info/org.prismlauncher.PrismLauncher.desktop.in
@@ -8,6 +8,6 @@ Exec=@Launcher_APP_BINARY_NAME@
StartupNotify=true
Icon=org.prismlauncher.PrismLauncher
Categories=Game;ActionGame;AdventureGame;Simulation;
-Keywords=game;minecraft;launcher;mc;multimc;polymc;
+Keywords=game;minecraft;mc;
StartupWMClass=PrismLauncher
MimeType=application/zip;application/x-modrinth-modpack+zip
diff --git a/program_info/org.prismlauncher.PrismLauncher.metainfo.xml.in b/program_info/org.prismlauncher.PrismLauncher.metainfo.xml.in
index 96708960..a482f0e3 100644
--- a/program_info/org.prismlauncher.PrismLauncher.metainfo.xml.in
+++ b/program_info/org.prismlauncher.PrismLauncher.metainfo.xml.in
@@ -10,7 +10,7 @@
<url type="homepage">https://prismlauncher.org/</url>
<url type="help">https://prismlauncher.org/wiki/</url>
<url type="bugtracker">https://github.com/PrismLauncher/PrismLauncher/issues</url>
- <url type="contact">https://discord.gg/prismlauncher</url>
+ <url type="contact">https://prismlauncher.org/discord</url>
<url type="vcs-browser">https://github.com/PrismLauncher/PrismLauncher</url>
<url type="contribute">https://github.com/PrismLauncher/PrismLauncher/blob/develop/CONTRIBUTING.md</url>
<url type="translate">https://hosted.weblate.org/projects/prismlauncher/launcher</url>
diff --git a/program_info/win_install.nsi.in b/program_info/win_install.nsi.in
index f8918c58..d3b5c256 100644
--- a/program_info/win_install.nsi.in
+++ b/program_info/win_install.nsi.in
@@ -12,6 +12,8 @@ OutFile "../@Launcher_CommonName@-Setup.exe"
!define MUI_ICON "../@Launcher_Branding_ICO@"
+!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\@Launcher_CommonName@"
+
;--------------------------------
; Pages
@@ -269,7 +271,73 @@ VIAddVersionKey /LANG=${LANG_ENGLISH} "ProductVersion" "@Launcher_VERSION_NAME4@
!macroend
-;--------------------------------
+;------------------------------------------
+; Uninstall Previous install
+
+!macro RunUninstall exitcode uninstcommand
+ Push `${uninstcommand}`
+ Call RunUninstall
+ Pop ${exitcode}
+!macroend
+
+; Checks that the uninstaller in the provided command exists and runs it.
+Function RunUninstall
+ Exch $1 ; input uninstcommand
+ Push $2 ; Uninstaller
+ Push $3 ; Len
+ Push $4 ; uninstcommand
+ StrCpy $4 $1 ; make a copy of the command for later
+ StrCpy $3 ""
+ StrCpy $2 $1 1 ; take first char of string
+ StrCmp $2 '"' quoteloop stringloop
+ stringloop: ; get string length
+ StrCpy $2 $1 1 $3 ; get next char
+ IntOp $3 $3 + 1 ; index += 1
+ StrCmp $2 "" +2 stringloop ; if empty exit loop
+ IntOp $3 $3 - 1 ; index -= 1
+ Goto run
+ quoteloop: ; get string length with quotes removed
+ StrCmp $3 "" 0 +2 ; if index is set skip quote removal
+ StrCpy $1 $1 "" 1 ; Remove initial quote
+ IntOp $3 $3 + 1 ; index += 1
+ StrCpy $2 $1 1 $3 ; get next char
+ StrCmp $2 "" +2 ; if empty exit loop
+ StrCmp $2 '"' 0 quoteloop ; if ending quote exit loop, else loop
+ run:
+ StrCpy $2 $1 $3 ; Path to uninstaller ; (copy string up to ending quote - if it exists)
+ StrCpy $1 161 ; ERROR_BAD_PATHNAME ; set exit code (it get's overwritten with uninstaller exit code if ExecWait call doesn't error)
+ GetFullPathName $3 "$2\.." ; $InstDir
+ IfFileExists "$2" 0 +4
+ ExecWait $4 $1 ; The file exists, call the saved command
+ IntCmp $1 0 "" +2 +2 ; Don't delete the installer if it was aborted ;
+ Delete "$2" ; Delete the uninstaller
+ RMDir "$3" ; Try to delete $InstDir
+ Pop $4
+ Pop $3
+ Pop $2
+ Exch $1 ; exitcode
+FunctionEnd
+
+; The "" makes the section hidden.
+Section "" UninstallPrevious
+
+ ReadRegStr $0 HKCU "${UNINST_KEY}" "QuietUninstallString"
+ ${If} $0 == ""
+ ReadRegStr $0 HKCU "${UNINST_KEY}" "UninstallString"
+ ${EndIf}
+
+ ${If} $0 != ""
+ !insertmacro RunUninstall $0 $0
+ ${If} $0 <> 0
+ MessageBox MB_YESNO|MB_ICONSTOP "Failed to uninstall, continue anyway?" /SD IDYES IDYES +2
+ Abort
+ ${EndIf}
+ ${EndIf}
+
+SectionEnd
+
+
+;------------------------------------
; The stuff to install
Section "@Launcher_DisplayName@"
@@ -283,6 +351,7 @@ Section "@Launcher_DisplayName@"
File "@Launcher_APP_BINARY_NAME@.exe"
File "@Launcher_APP_BINARY_NAME@_filelink.exe"
File "qt.conf"
+ File "qtlogging.ini"
File *.dll
File /r "iconengines"
File /r "imageformats"
@@ -298,11 +367,10 @@ Section "@Launcher_DisplayName@"
${GetParameters} $R0
${GetOptions} $R0 "/NoUninstaller" $R1
${If} ${Errors}
- !define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\@Launcher_CommonName@"
WriteRegStr HKCU "${UNINST_KEY}" "DisplayName" "@Launcher_DisplayName@"
WriteRegStr HKCU "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\@Launcher_APP_BINARY_NAME@.exe"
- WriteRegStr HKCU "${UNINST_KEY}" "UninstallString" '"$INSTDIR\uninstall.exe"'
- WriteRegStr HKCU "${UNINST_KEY}" "QuietUninstallString" '"$INSTDIR\uninstall.exe" /S'
+ WriteRegStr HKCU "${UNINST_KEY}" "UninstallString" '"$INSTDIR\uninstall.exe" _?=$INSTDIR'
+ WriteRegStr HKCU "${UNINST_KEY}" "QuietUninstallString" '"$INSTDIR\uninstall.exe" /S _?=$INSTDIR'
WriteRegStr HKCU "${UNINST_KEY}" "InstallLocation" "$INSTDIR"
WriteRegStr HKCU "${UNINST_KEY}" "Publisher" "@Launcher_DisplayName@ Contributors"
WriteRegStr HKCU "${UNINST_KEY}" "Version" "@Launcher_VERSION_NAME4@"
diff --git a/renovate.json b/renovate.json
index 39a2b6e9..d97a8dc6 100644
--- a/renovate.json
+++ b/renovate.json
@@ -2,5 +2,11 @@
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:base"
- ]
+ ],
+ "nix": {
+ "enabled": true
+ },
+ "lockFileMaintenance": {
+ "enabled": true
+ }
}
diff --git a/tests/INIFile_test.cpp b/tests/INIFile_test.cpp
index 4be8133c..95730e24 100644
--- a/tests/INIFile_test.cpp
+++ b/tests/INIFile_test.cpp
@@ -1,24 +1,19 @@
#include <QTest>
+#include <settings/INIFile.h>
#include <QList>
+#include <QSettings>
+#include <QTemporaryFile>
#include <QVariant>
-#include <settings/INIFile.h>
+#include "FileSystem.h"
#include <QVariantUtils.h>
-class IniFileTest : public QObject
-{
+class IniFileTest : public QObject {
Q_OBJECT
-private
-slots:
- void initTestCase()
- {
-
- }
- void cleanupTestCase()
- {
-
- }
+ private slots:
+ void initTestCase() {}
+ void cleanupTestCase() {}
void test_Escape_data()
{
@@ -47,17 +42,17 @@ slots:
// load
INIFile f2;
f2.loadFile(filename);
- QCOMPARE(f2.get("a","NOT SET").toString(), a);
- QCOMPARE(f2.get("b","NOT SET").toString(), b);
+ QCOMPARE(f2.get("a", "NOT SET").toString(), a);
+ QCOMPARE(f2.get("b", "NOT SET").toString(), b);
}
void test_SaveLoadLists()
{
QString slist_strings = "(\"a\",\"b\",\"c\")";
- QStringList list_strings = {"a", "b", "c"};
+ QStringList list_strings = { "a", "b", "c" };
QString slist_numbers = "(1,2,3,10)";
- QList<int> list_numbers = {1, 2, 3, 10};
+ QList<int> list_numbers = { 1, 2, 3, 10 };
QString filename = "test_SaveLoadLists.ini";
@@ -72,13 +67,129 @@ slots:
QStringList out_list_strings = f2.get("list_strings", QStringList()).toStringList();
qDebug() << "OutStringList" << out_list_strings;
-
+
QList<int> out_list_numbers = QVariantUtils::toList<int>(f2.get("list_numbers", QVariantUtils::fromList(QList<int>())));
qDebug() << "OutNumbersList" << out_list_numbers;
QCOMPARE(out_list_strings, list_strings);
QCOMPARE(out_list_numbers, list_numbers);
}
+
+ void test_SaveAlreadyExistingFile()
+ {
+ QString fileContent = R"(InstanceType=OneSix
+iconKey=vanillia_icon
+name=Minecraft Vanillia
+OverrideCommands=true
+PreLaunchCommand="$INST_JAVA" -jar packwiz-installer-bootstrap.jar link
+Wrapperommand=)";
+ fileContent += "\"";
+ fileContent += +R"(\"$INST_JAVA\" -jar packwiz-installer-bootstrap.jar link =)";
+ fileContent += "\"\n";
+#if defined(Q_OS_WIN)
+ QString fileName = "test_SaveAlreadyExistingFile.ini";
+ QFile file(fileName);
+ QCOMPARE(file.open(QFile::WriteOnly | QFile::Text), true);
+#else
+ QTemporaryFile file;
+ QCOMPARE(file.open(), true);
+ QCOMPARE(file.fileName().isEmpty(), false);
+ QString fileName = file.fileName();
+#endif
+ QTextStream stream(&file);
+ stream << fileContent;
+ file.close();
+
+ // load
+ INIFile f1;
+ f1.loadFile(fileName);
+ QCOMPARE(f1.get("PreLaunchCommand", "NOT SET").toString(), "\"$INST_JAVA\" -jar packwiz-installer-bootstrap.jar link");
+ QCOMPARE(f1.get("Wrapperommand", "NOT SET").toString(), "\"$INST_JAVA\" -jar packwiz-installer-bootstrap.jar link =");
+ f1.saveFile(fileName);
+ INIFile f2;
+ f2.loadFile(fileName);
+ QCOMPARE(f2.get("PreLaunchCommand", "NOT SET").toString(), "\"$INST_JAVA\" -jar packwiz-installer-bootstrap.jar link");
+ QCOMPARE(f2.get("Wrapperommand", "NOT SET").toString(), "\"$INST_JAVA\" -jar packwiz-installer-bootstrap.jar link =");
+ QCOMPARE(f2.get("ConfigVersion", "NOT SET").toString(), "1.2");
+#if defined(Q_OS_WIN)
+ FS::deletePath(fileName);
+#endif
+ }
+
+ void test_SaveAlreadyExistingFileWithSpecialChars()
+ {
+#if defined(Q_OS_WIN)
+ QString fileName = "test_SaveAlreadyExistingFileWithSpecialChars.ini";
+#else
+ QTemporaryFile file;
+ QCOMPARE(file.open(), true);
+ QCOMPARE(file.fileName().isEmpty(), false);
+ QString fileName = file.fileName();
+ file.close();
+#endif
+ QSettings settings{ fileName, QSettings::Format::IniFormat };
+ settings.setFallbacksEnabled(false);
+
+ settings.setValue("simple", "value1");
+ settings.setValue("withQuotes", R"("value2" with quotes)");
+ settings.setValue("withSpecialCharacters", "env mesa=true");
+ settings.setValue("withSpecialCharacters2", "1,2,3,4");
+ settings.setValue("withSpecialCharacters2", "1;2;3;4");
+ settings.setValue("withAll", "val=\"$INST_JAVA\" -jar; ls ");
+
+ settings.sync();
+
+ QCOMPARE(settings.status(), QSettings::Status::NoError);
+
+ // load
+ INIFile f1;
+ f1.loadFile(fileName);
+ for (auto key : settings.allKeys())
+ QCOMPARE(f1.get(key, "NOT SET").toString(), settings.value(key).toString());
+ f1.saveFile(fileName);
+ INIFile f2;
+ f2.loadFile(fileName);
+ for (auto key : settings.allKeys())
+ QCOMPARE(f2.get(key, "NOT SET").toString(), settings.value(key).toString());
+ QCOMPARE(f2.get("ConfigVersion", "NOT SET").toString(), "1.2");
+#if defined(Q_OS_WIN)
+ FS::deletePath(fileName);
+#endif
+ }
+
+ void test_SaveAlreadyExistingFileWithSpecialCharsV1()
+ {
+ QString fileContent = R"(InstanceType=OneSix
+ConfigVersion=1.1
+iconKey=vanillia_icon
+name=Minecraft Vanillia
+OverrideCommands=true
+PreLaunchCommand=)";
+ fileContent += "\"\\\"env mesa=true\\\"\"\n";
+
+#if defined(Q_OS_WIN)
+ QString fileName = "test_SaveAlreadyExistingFileWithSpecialCharsV1.ini";
+ QFile file(fileName);
+ QCOMPARE(file.open(QFile::WriteOnly | QFile::Text), true);
+#else
+ QTemporaryFile file;
+ QCOMPARE(file.open(), true);
+ QCOMPARE(file.fileName().isEmpty(), false);
+ QString fileName = file.fileName();
+#endif
+ QTextStream stream(&file);
+ stream << fileContent;
+ file.close();
+
+ // load
+ INIFile f1;
+ f1.loadFile(fileName);
+ QCOMPARE(f1.get("PreLaunchCommand", "NOT SET").toString(), "env mesa=true");
+ QCOMPARE(f1.get("ConfigVersion", "NOT SET").toString(), "1.2");
+#if defined(Q_OS_WIN)
+ FS::deletePath(fileName);
+#endif
+ }
};
QTEST_GUILESS_MAIN(IniFileTest)
diff --git a/tests/ResourceFolderModel_test.cpp b/tests/ResourceFolderModel_test.cpp
index 054d81c4..962d89f1 100644
--- a/tests/ResourceFolderModel_test.cpp
+++ b/tests/ResourceFolderModel_test.cpp
@@ -90,9 +90,7 @@ slots:
QEventLoop loop;
- InstancePtr instance;
-
- ModFolderModel m(tempDir.path(), instance, true);
+ ModFolderModel m(tempDir.path(), nullptr, true);
connect(&m, &ModFolderModel::updateFinished, &loop, &QEventLoop::quit);
@@ -116,8 +114,7 @@ slots:
QString folder = source + '/';
QTemporaryDir tempDir;
QEventLoop loop;
- InstancePtr instance;
- ModFolderModel m(tempDir.path(), instance, true);
+ ModFolderModel m(tempDir.path(), nullptr, true);
connect(&m, &ModFolderModel::updateFinished, &loop, &QEventLoop::quit);
@@ -140,8 +137,7 @@ slots:
void test_addFromWatch()
{
QString source = QFINDTESTDATA("testdata/ResourceFolderModel");
- InstancePtr instance;
- ModFolderModel model(source, instance);
+ ModFolderModel model(source, nullptr);
QCOMPARE(model.size(), 0);
@@ -161,9 +157,7 @@ slots:
QString file_mod = QFINDTESTDATA("testdata/ResourceFolderModel/supercoolmod.jar");
QTemporaryDir tmp;
- InstancePtr instance;
-
- ResourceFolderModel model(QDir(tmp.path()), instance);
+ ResourceFolderModel model(QDir(tmp.path()), nullptr);
QCOMPARE(model.size(), 0);
@@ -214,8 +208,7 @@ slots:
QString file_mod = QFINDTESTDATA("testdata/ResourceFolderModel/supercoolmod.jar");
QTemporaryDir tmp;
- InstancePtr instance;
- ResourceFolderModel model(tmp.path(), instance);
+ ResourceFolderModel model(tmp.path(), nullptr);
QCOMPARE(model.size(), 0);
diff --git a/tests/ResourceModel_test.cpp b/tests/ResourceModel_test.cpp
index 716bf853..c0d9cd95 100644
--- a/tests/ResourceModel_test.cpp
+++ b/tests/ResourceModel_test.cpp
@@ -75,9 +75,9 @@ class ResourceModelTest : public QObject {
auto search_json = DummyResourceAPI::searchRequestResult();
auto processed_response = model->documentToArray(search_json).first().toObject();
- QVERIFY(processed_pack.addonId.toString() == Json::requireString(processed_response, "project_id"));
- QVERIFY(processed_pack.description == Json::requireString(processed_response, "description"));
- QVERIFY(processed_pack.authors.first().name == Json::requireString(processed_response, "author"));
+ QVERIFY(processed_pack->addonId.toString() == Json::requireString(processed_response, "project_id"));
+ QVERIFY(processed_pack->description == Json::requireString(processed_response, "description"));
+ QVERIFY(processed_pack->authors.first().name == Json::requireString(processed_response, "author"));
}
};
diff --git a/tests/Task_test.cpp b/tests/Task_test.cpp
index 95eb4a30..dabe5da2 100644
--- a/tests/Task_test.cpp
+++ b/tests/Task_test.cpp
@@ -69,8 +69,9 @@ class BigConcurrentTaskThread : public QThread {
auto sub_tasks = new BasicTask::Ptr[s_num_tasks];
for (unsigned i = 0; i < s_num_tasks; i++) {
- sub_tasks[i] = makeShared<BasicTask>(false);
- big_task.addTask(sub_tasks[i]);
+ auto sub_task = makeShared<BasicTask>(false);
+ sub_tasks[i] = sub_task;
+ big_task.addTask(sub_task);
}
big_task.run();
@@ -99,7 +100,7 @@ class TaskTest : public QObject {
t.setStatus(status);
QCOMPARE(t.getStatus(), status);
- QCOMPARE(t.getStepStatus(), status);
+ QCOMPARE(t.getStepProgress().isEmpty(), TaskStepProgressList{}.isEmpty());
}
void test_SetStatus_MultiStep(){
@@ -111,7 +112,7 @@ class TaskTest : public QObject {
QCOMPARE(t.getStatus(), status);
// Even though it is multi step, it does not override the getStepStatus method,
// so it should remain the same.
- QCOMPARE(t.getStepStatus(), status);
+ QCOMPARE(t.getStepProgress().isEmpty(), TaskStepProgressList{}.isEmpty());
}
void test_SetProgress(){