aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/build.yml27
-rw-r--r--.github/workflows/trigger_release.yml2
-rw-r--r--CMakeLists.txt2
-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.cpp41
-rw-r--r--launcher/CMakeLists.txt9
-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/InstanceTask.cpp5
-rw-r--r--launcher/ResourceDownloadTask.cpp49
-rw-r--r--launcher/ResourceDownloadTask.h58
-rw-r--r--launcher/VersionProxyModel.cpp29
-rw-r--r--launcher/VersionProxyModel.h3
-rw-r--r--launcher/java/JavaChecker.cpp4
-rw-r--r--launcher/java/JavaInstallList.cpp5
-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.cpp3
-rw-r--r--launcher/minecraft/OneSixVersionFormat.cpp10
-rw-r--r--launcher/minecraft/VersionFile.h2
-rw-r--r--launcher/minecraft/auth/AccountList.cpp6
-rw-r--r--launcher/minecraft/mod/DataPack.cpp4
-rw-r--r--launcher/minecraft/mod/ResourcePack.cpp3
-rw-r--r--launcher/modplatform/ModIndex.h35
-rw-r--r--launcher/modplatform/atlauncher/ATLPackInstallTask.cpp2
-rw-r--r--launcher/modplatform/flame/FlameCheckUpdate.cpp22
-rw-r--r--launcher/modplatform/flame/FlameInstanceCreationTask.cpp3
-rw-r--r--launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp28
-rw-r--r--launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp2
-rw-r--r--launcher/modplatform/modrinth/ModrinthPackExportTask.cpp319
-rw-r--r--launcher/modplatform/modrinth/ModrinthPackExportTask.h77
-rw-r--r--launcher/net/ByteArraySink.h15
-rw-r--r--launcher/settings/INIFile.cpp109
-rw-r--r--launcher/tasks/ConcurrentTask.cpp3
-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.ui32
-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/ResourceDownloadDialog.cpp88
-rw-r--r--launcher/ui/dialogs/ResourceDownloadDialog.h17
-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.cpp54
-rw-r--r--launcher/ui/pages/instance/ExternalResourcesPage.h1
-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/ModFolderPage.cpp27
-rw-r--r--launcher/ui/pages/modplatform/ModModel.cpp16
-rw-r--r--launcher/ui/pages/modplatform/ModModel.h5
-rw-r--r--launcher/ui/pages/modplatform/ModPage.cpp43
-rw-r--r--launcher/ui/pages/modplatform/ModPage.h15
-rw-r--r--launcher/ui/pages/modplatform/ResourceModel.cpp68
-rw-r--r--launcher/ui/pages/modplatform/ResourceModel.h14
-rw-r--r--launcher/ui/pages/modplatform/ResourcePage.cpp105
-rw-r--r--launcher/ui/pages/modplatform/ResourcePage.h18
-rw-r--r--launcher/ui/pages/modplatform/ShaderPackPage.cpp16
-rw-r--r--launcher/ui/pages/modplatform/ShaderPackPage.h4
-rw-r--r--launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.cpp2
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameModel.cpp2
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp2
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameResourceModels.h2
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp2
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp2
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h2
-rw-r--r--launcher/ui/themes/SystemTheme.cpp2
-rw-r--r--launcher/ui/widgets/JavaSettingsWidget.cpp2
-rw-r--r--launcher/ui/widgets/PageContainer.cpp9
-rw-r--r--launcher/ui/widgets/PageContainer.h1
-rw-r--r--launcher/ui/widgets/ProjectItem.cpp11
-rw-r--r--launcher/ui/widgets/ProjectItem.h3
-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--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/win_install.nsi.in75
-rw-r--r--renovate.json8
-rw-r--r--tests/INIFile_test.cpp147
101 files changed, 2552 insertions, 1028 deletions
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 691e257b..c2966abe 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -86,11 +86,11 @@ jobs:
- os: macos-12
name: macOS
- macosx_deployment_target: 10.15
+ macosx_deployment_target: 11.0
qt_ver: 6
qt_host: mac
qt_arch: ''
- qt_version: '6.5.1'
+ qt_version: '6.5.0'
qt_modules: 'qt5compat qtimageformats'
qt_tools: ''
@@ -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@v21
+ 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 6bd12630..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}")
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 1659eb44..724e6e44 100644
--- a/launcher/Application.cpp
+++ b/launcher/Application.cpp
@@ -376,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);
@@ -1699,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/CMakeLists.txt b/launcher/CMakeLists.txt
index 273b5449..ce2771a4 100644
--- a/launcher/CMakeLists.txt
+++ b/launcher/CMakeLists.txt
@@ -525,6 +525,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
@@ -720,6 +722,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
@@ -900,6 +906,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
@@ -1046,6 +1054,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
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/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/ResourceDownloadTask.cpp b/launcher/ResourceDownloadTask.cpp
index 61b918aa..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,13 +41,13 @@ 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);
}
}
diff --git a/launcher/ResourceDownloadTask.h b/launcher/ResourceDownloadTask.h
index 73ad2d07..09147c8c 100644
--- a/launcher/ResourceDownloadTask.h
+++ b/launcher/ResourceDownloadTask.h
@@ -1,44 +1,51 @@
// 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 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 +54,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/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/java/JavaChecker.cpp b/launcher/java/JavaChecker.cpp
index b4c55b3d..e4a686c2 100644
--- a/launcher/java/JavaChecker.cpp
+++ b/launcher/java/JavaChecker.cpp
@@ -85,7 +85,7 @@ 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(), QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this, &JavaChecker::finished);
connect(process.get(), &QProcess::errorOccurred, this, &JavaChecker::error);
@@ -128,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/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/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 2c624a36..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);
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/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/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/ResourcePack.cpp b/launcher/minecraft/mod/ResourcePack.cpp
index 9aea22ef..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)
diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h
index 8d0223f9..82da2ab2 100644
--- a/launcher/modplatform/ModIndex.h
+++ b/launcher/modplatform/ModIndex.h
@@ -1,20 +1,20 @@
// 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/>.
-*/
+ * PolyMC - Minecraft Launcher
+ * Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
#pragma once
@@ -69,7 +69,6 @@ struct IndexedVersion {
// For internal use, not provided by APIs
bool is_currently_selected = false;
- QString custom_target_folder;
};
struct ExtraPackData {
@@ -116,12 +115,12 @@ 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; });
}
};
} // namespace ModPlatform
Q_DECLARE_METATYPE(ModPlatform::IndexedPack)
+Q_DECLARE_METATYPE(ModPlatform::IndexedPack::Ptr)
Q_DECLARE_METATYPE(ModPlatform::ResourceProvider)
diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp
index 96cea7b7..07e0bf23 100644
--- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp
+++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp
@@ -352,7 +352,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
diff --git a/launcher/modplatform/flame/FlameCheckUpdate.cpp b/launcher/modplatform/flame/FlameCheckUpdate.cpp
index 06a89502..e09aeb3d 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"
@@ -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 dae93d1c..35443e71 100644
--- a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp
+++ b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp
@@ -153,6 +153,9 @@ bool FlameCreationTask::updateInstance()
old_files.remove(file.key());
files_iterator = files.erase(files_iterator);
+
+ if (files_iterator != files.begin())
+ files_iterator--;
}
}
diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp
index d1be7209..4fe91ce7 100644
--- a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp
+++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp
@@ -54,7 +54,7 @@ void ModrinthCheckUpdate::executeTask()
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());
+ QString hash(hash_task->getResult());
hashes.append(hash);
mappings.insert(hash, mod);
});
@@ -67,7 +67,7 @@ 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();
@@ -112,7 +112,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 +123,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 +151,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 bb8227aa..76f07277 100644
--- a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp
+++ b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp
@@ -214,7 +214,7 @@ bool ModrinthCreationTask::createInstance()
if (m_instIcon != "default") {
instance.setIconKey(m_instIcon);
- } else {
+ } else if (!m_managed_id.isEmpty()) {
instance.setIconKey("modrinth");
}
diff --git a/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp b/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp
new file mode 100644
index 00000000..bff9bf42
--- /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 file{ sha1.result().toHex(), sha512.result().toHex(), url.toString(), openFile.size() };
+ resolvedFiles[relative] = file;
+
+ // 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 {
+ QByteArray* response = new 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 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..af00ffaa
--- /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 QByteArray* response);
+ void buildZip();
+ void finish();
+
+ QByteArray generateIndex();
+};
diff --git a/launcher/net/ByteArraySink.h b/launcher/net/ByteArraySink.h
index 501318a1..728193b3 100644
--- a/launcher/net/ByteArraySink.h
+++ b/launcher/net/ByteArraySink.h
@@ -53,7 +53,10 @@ class ByteArraySink : public Sink {
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 +64,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 +75,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;
}
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/tasks/ConcurrentTask.cpp b/launcher/tasks/ConcurrentTask.cpp
index 5ee14505..9aada5e6 100644
--- a/launcher/tasks/ConcurrentTask.cpp
+++ b/launcher/tasks/ConcurrentTask.cpp
@@ -138,19 +138,18 @@ void ConcurrentTask::startNext()
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);
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);
}
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 72b7db64..e04011ca 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
@@ -414,15 +419,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());
@@ -432,6 +428,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());
@@ -471,7 +473,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)
@@ -673,6 +691,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();
@@ -687,6 +714,8 @@ void MainWindow::repopulateAccountsMenu()
}
}
+ QActionGroup* accountsGroup = new QActionGroup(this);
+
if (accounts->count() <= 0)
{
ui->actionNoAccountsAdded->setEnabled(false);
@@ -702,6 +731,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);
@@ -730,6 +760,7 @@ void MainWindow::repopulateAccountsMenu()
ui->actionNoDefaultAccount->setData(-1);
ui->actionNoDefaultAccount->setChecked(!defaultAccount);
+ ui->actionNoDefaultAccount->setActionGroup(accountsGroup);
ui->accountsMenu->addAction(ui->actionNoDefaultAccount);
@@ -737,6 +768,8 @@ void MainWindow::repopulateAccountsMenu()
ui->accountsMenu->addSeparator();
ui->accountsMenu->addAction(ui->actionManageAccounts);
+
+ accountsButtonMenu->addActions(ui->accountsMenu->actions());
}
void MainWindow::updatesAllowedChanged(bool allowed)
@@ -1201,6 +1234,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();
@@ -1359,7 +1398,7 @@ void MainWindow::on_actionDeleteInstance_triggered()
APPLICATION->instances()->deleteInstance(id);
}
-void MainWindow::on_actionExportInstance_triggered()
+void MainWindow::on_actionExportInstanceZip_triggered()
{
if (m_selectedInstance)
{
@@ -1368,6 +1407,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..f67fb185 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">
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..561b92e4
--- /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).+$");
+
+ 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/ResourceDownloadDialog.cpp b/launcher/ui/dialogs/ResourceDownloadDialog.cpp
index d2a8d33e..6d90480f 100644
--- a/launcher/ui/dialogs/ResourceDownloadDialog.cpp
+++ b/launcher/ui/dialogs/ResourceDownloadDialog.cpp
@@ -20,14 +20,15 @@
#include "ResourceDownloadDialog.h"
#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 "ui/dialogs/ReviewMessageBox.h"
@@ -41,7 +42,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"));
@@ -102,7 +106,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);
@@ -114,21 +119,24 @@ void ResourceDownloadDialog::connectButtons()
void ResourceDownloadDialog::confirm()
{
- auto keys = m_selected.keys();
- keys.sort(Qt::CaseInsensitive);
+ 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;
+ });
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() });
+ for (auto& task : selected) {
+ confirm_dialog->appendResource({ task->getName(), task->getFilename(), task->getCustomPath() });
}
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 +153,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 +206,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,7 +231,6 @@ QList<BasePage*> ModDownloadDialog::getPages()
return pages;
}
-
ResourcePackDownloadDialog::ResourcePackDownloadDialog(QWidget* parent,
const std::shared_ptr<ResourcePackFolderModel>& resource_packs,
BaseInstance* instance)
@@ -255,10 +253,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 +280,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 +305,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..5b5b48c6 100644
--- a/launcher/ui/dialogs/ResourceDownloadDialog.h
+++ b/launcher/ui/dialogs/ResourceDownloadDialog.h
@@ -62,8 +62,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 +79,7 @@ class ResourceDownloadDialog : public QDialog, public BasePageProvider {
protected:
[[nodiscard]] virtual QString geometrySaveKey() const { return ""; }
+ void setButtonStatus();
protected:
const std::shared_ptr<ResourceFolderModel> m_base_model;
@@ -88,12 +89,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
@@ -135,8 +132,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 +150,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/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..5537c28f 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)>;
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 2f824ffb..8e5226ef 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)
@@ -45,6 +81,13 @@ 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(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();
@@ -263,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 906e6df7..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;
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/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/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp
index afd8b292..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;
}
@@ -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..805d8b9a 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);
@@ -42,9 +42,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 4ea55efa..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());
@@ -48,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 472aa851..49405a02 100644
--- a/launcher/ui/pages/modplatform/ResourceModel.cpp
+++ b/launcher/ui/pages/modplatform/ResourceModel.cpp
@@ -6,9 +6,11 @@
#include <QCryptographicHash>
#include <QIcon>
+#include <QList>
#include <QMessageBox>
#include <QPixmapCache>
#include <QUrl>
+#include <algorithm>
#include <memory>
#include "Application.h"
@@ -65,7 +67,7 @@ auto ResourceModel::data(const QModelIndex& index, int role) const -> QVariant
return QSize(0, 58);
case Qt::UserRole: {
QVariant v;
- v.setValue(*pack);
+ v.setValue(pack);
return v;
}
// Custom data
@@ -75,6 +77,8 @@ auto ResourceModel::data(const QModelIndex& index, int role) const -> QVariant
return pack->description;
case UserDataTypes::SELECTED:
return pack->isAnyVersionSelected();
+ case UserDataTypes::INSTALLED:
+ return this->isPackInstalled(pack);
default:
break;
}
@@ -93,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;
}
@@ -103,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] = std::make_shared<ModPlatform::IndexedPack>(value.value<ModPlatform::IndexedPack>());
+ m_packs[pos] = value.value<ModPlatform::IndexedPack::Ptr>();
emit dataChanged(index, index);
return true;
@@ -230,7 +235,7 @@ void ResourceModel::clearData()
void ResourceModel::runSearchJob(Task::Ptr ptr)
{
- m_current_search_job.reset(ptr); // clean up first
+ m_current_search_job.reset(ptr); // clean up first
m_current_search_job->start();
}
void ResourceModel::runInfoJob(Task::Ptr ptr)
@@ -336,7 +341,15 @@ void ResourceModel::searchRequestSucceeded(QJsonDocument& doc)
ModPlatform::IndexedPack::Ptr pack = std::make_shared<ModPlatform::IndexedPack>();
try {
loadIndexedPack(*pack, packObj);
- newList.append(pack);
+ 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;
@@ -390,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();
@@ -417,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();
@@ -442,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 1ec42cda..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;
@@ -124,6 +137,7 @@ class ResourceModel : public QAbstractListModel {
QSet<QUrl> m_failed_icon_actions;
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/ResourcePage.cpp b/launcher/ui/pages/modplatform/ResourcePage.cpp
index f75bb886..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>
@@ -158,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;
@@ -190,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();
}
@@ -239,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";
}
}
@@ -254,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));
@@ -279,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);
@@ -288,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)
@@ -308,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()
@@ -324,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);
@@ -340,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)
@@ -370,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/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 9039c4d9..fcf6d4a7 100644
--- a/launcher/ui/pages/modplatform/ShaderPackPage.h
+++ b/launcher/ui/pages/modplatform/ShaderPackPage.h
@@ -38,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/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/flame/FlameModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModel.cpp
index 5961ea02..d9d5ef5b 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;
}
diff --git a/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp b/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp
index e3d0bc14..667a52d0 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)
{
diff --git a/launcher/ui/pages/modplatform/flame/FlameResourceModels.h b/launcher/ui/pages/modplatform/flame/FlameResourceModels.h
index 0252ac40..221c8f7a 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:
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp
index 346a00b0..55d287b0 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;
}
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp
index f5d1cc28..7f857485 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)
{
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h
index b351b19b..66461807 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:
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/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/PageContainer.cpp b/launcher/ui/widgets/PageContainer.cpp
index b9b17b42..34df42ec 100644
--- a/launcher/ui/widgets/PageContainer.cpp
+++ b/launcher/ui/widgets/PageContainer.cpp
@@ -93,6 +93,10 @@ PageContainer::PageContainer(BasePageProvider *pageProvider, QString defaultId,
page->listIndex = counter;
page->setParentContainer(this);
counter++;
+ page->updateExtraInfo = [this](QString info) {
+ if (m_currentPage)
+ m_header->setText(m_currentPage->displayName() + info);
+ };
}
m_model->setPages(pages);
@@ -137,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/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/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/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/win_install.nsi.in b/program_info/win_install.nsi.in
index 1d902d5d..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@"
@@ -299,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)