diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 000000000..2163db45b --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,4 @@ +# .git-blame-ignore-revs + +# tabs -> spaces +bbb3b3e6f6e3c0f95873f22e6d0a4aaf350f49d9 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 691e257bf..c2966abe7 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 3c56a38ea..f19b83986 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 6bd12630d..70a553190 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 353731896..8a412b7ff 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 a05d7a9eb..8543d7241 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 * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2023 TheKodeToad * * 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 +#include /** * \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 146942d59..c7d0c267d 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 ad9196a9b..875866438 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 f656703ce..c3148fe03 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 1659eb445..1d97a5f2e 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(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); @@ -594,7 +594,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) // Java Settings m_settings->registerSetting("JavaPath", ""); - m_settings->registerSetting("JavaTimestamp", 0); + m_settings->registerSetting("JavaSignature", ""); m_settings->registerSetting("JavaArchitecture", ""); m_settings->registerSetting("JavaRealArchitecture", ""); m_settings->registerSetting("JavaVersion", ""); @@ -1699,6 +1699,7 @@ bool Application::handleDataMigration(const QString& currentData, matcher->add(std::make_shared(configFile)); matcher->add(std::make_shared( BuildConfig.LAUNCHER_CONFIGFILE)); // it's possible that we already used that directory before + matcher->add(std::make_shared("logs/")); matcher->add(std::make_shared("accounts.json")); matcher->add(std::make_shared("accounts/")); matcher->add(std::make_shared("assets/")); diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 5f3591b13..9bb465721 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -362,6 +362,8 @@ set(MINECRAFT_SOURCES minecraft/mod/tasks/LocalWorldSaveParseTask.cpp minecraft/mod/tasks/LocalResourceParse.h minecraft/mod/tasks/LocalResourceParse.cpp + minecraft/mod/tasks/GetModDependenciesTask.h + minecraft/mod/tasks/GetModDependenciesTask.cpp # Assets minecraft/AssetsUtils.h @@ -375,8 +377,6 @@ set(MINECRAFT_SOURCES minecraft/services/SkinDelete.cpp minecraft/services/SkinDelete.h - mojang/PackageManifest.h - mojang/PackageManifest.cpp minecraft/Agent.h) # the screenshots feature @@ -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 @@ -680,6 +682,7 @@ SET(LAUNCHER_SOURCES VersionProxyModel.h VersionProxyModel.cpp Markdown.h + Markdown.cpp # Super secret! KonamiCode.h @@ -720,6 +723,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 @@ -819,8 +826,8 @@ SET(LAUNCHER_SOURCES ui/pages/global/APIPage.h # GUI - platform pages - ui/pages/modplatform/VanillaPage.cpp - ui/pages/modplatform/VanillaPage.h + ui/pages/modplatform/CustomPage.cpp + ui/pages/modplatform/CustomPage.h ui/pages/modplatform/ResourcePage.cpp ui/pages/modplatform/ResourcePage.h @@ -900,6 +907,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 @@ -1024,7 +1033,7 @@ qt_wrap_ui(LAUNCHER_UI ui/pages/instance/ScreenshotsPage.ui ui/pages/modplatform/atlauncher/AtlOptionalModDialog.ui ui/pages/modplatform/atlauncher/AtlPage.ui - ui/pages/modplatform/VanillaPage.ui + ui/pages/modplatform/CustomPage.ui ui/pages/modplatform/ResourcePage.ui ui/pages/modplatform/flame/FlamePage.ui ui/pages/modplatform/legacy_ftb/Page.ui @@ -1046,6 +1055,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 000000000..f2b6f4425 --- /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 + * + * 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 . + */ + +#include "FastFileIconProvider.h" + +#include +#include + +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 000000000..208534044 --- /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 + * + * 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 . + */ + +#pragma once + +#include + +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 000000000..4c8c64c72 --- /dev/null +++ b/launcher/FileIgnoreProxy.cpp @@ -0,0 +1,279 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2023 TheKodeToad + * + * 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 . + * + * 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 +#include +#include +#include +#include +#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(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(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(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(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 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 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(sourceModel()); + if (!fsm) { + return false; + } + auto blockedPath = relPath(fsm->filePath(sourceIndex)); + auto found = blocked.find(blockedPath); + if (found) { + return !found->leaf(); + } + return false; +} + +void FileIgnoreProxy::setBlockedPaths(QStringList paths) +{ + beginResetModel(); + blocked.clear(); + blocked.insert(paths); + endResetModel(); +} + +bool FileIgnoreProxy::filterAcceptsColumn(int source_column, const QModelIndex& source_parent) const +{ + Q_UNUSED(source_parent) + + // adjust the columns you want to filter out here + // return false for those that will be hidden + if (source_column == 2 || source_column == 3) + return false; + + return true; +} + +bool FileIgnoreProxy::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const +{ + QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); + QFileSystemModel* fsm = qobject_cast(sourceModel()); + + auto fileInfo = fsm->fileInfo(index); + return !ignoreFile(fileInfo); +} + +bool FileIgnoreProxy::ignoreFile(QFileInfo fileInfo) const +{ + auto fileName = fileInfo.fileName(); + auto path = relPath(fileInfo.absoluteFilePath()); + return std::any_of(m_ignoreFiles.cbegin(), m_ignoreFiles.cend(), [fileName](auto iFileName) { return fileName == iFileName; }) || + m_ignoreFilePaths.covers(path); +} + +bool FileIgnoreProxy::filterFile(const QString& fileName) const +{ + return blocked.covers(fileName) || ignoreFile(QFileInfo(QDir(root), fileName)); +} diff --git a/launcher/FileIgnoreProxy.h b/launcher/FileIgnoreProxy.h new file mode 100644 index 000000000..e01a2651e --- /dev/null +++ b/launcher/FileIgnoreProxy.h @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2023 TheKodeToad + * + * 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 . + * + * 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 +#include +#include "SeparatorPrefixTree.h" + +class FileIgnoreProxy : public QSortFilterProxyModel { + Q_OBJECT + + public: + FileIgnoreProxy(QString root, QObject* parent); + // NOTE: Sadly, we have to do sorting ourselves. + bool lessThan(const QModelIndex& left, const QModelIndex& right) const; + + virtual Qt::ItemFlags flags(const QModelIndex& index) const; + + virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const; + virtual bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole); + + QString relPath(const QString& path) const; + + bool setFilterState(QModelIndex index, Qt::CheckState state); + + bool shouldExpand(QModelIndex index); + + void setBlockedPaths(QStringList paths); + + inline const SeparatorPrefixTree<'/'>& blockedPaths() const { return blocked; } + inline SeparatorPrefixTree<'/'>& blockedPaths() { return blocked; } + + // list of file names that need to be removed completely from model + inline QStringList& ignoreFilesWithName() { return m_ignoreFiles; } + // list of relative paths that need to be removed completely from model + inline SeparatorPrefixTree<'/'>& ignoreFilesWithPath() { return m_ignoreFilePaths; } + + bool filterFile(const QString& fileName) const; + + protected: + bool filterAcceptsColumn(int source_column, const QModelIndex& source_parent) const; + bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const; + + bool ignoreFile(QFileInfo file) const; + + private: + const QString root; + SeparatorPrefixTree<'/'> blocked; + QStringList m_ignoreFiles; + SeparatorPrefixTree<'/'> m_ignoreFilePaths; +}; diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp index d98526dfd..1ea9f755a 100644 --- a/launcher/FileSystem.cpp +++ b/launcher/FileSystem.cpp @@ -102,7 +102,7 @@ namespace fs = ghc::filesystem; #include #include #include -#elif defined(Q_OS_MACOS) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) +#elif defined(Q_OS_MACOS) || defined(Q_OS_OPENBSD) #include #include #elif defined(Q_OS_WIN) @@ -372,7 +372,7 @@ void create_link::make_link_list(const QString& offset) auto src_path = source_it.next(); auto relative_path = src_dir.relativeFilePath(src_path); - if (m_max_depth >= 0 && pathDepth(relative_path) > m_max_depth){ + if (m_max_depth >= 0 && pathDepth(relative_path) > m_max_depth) { relative_path = pathTruncate(relative_path, m_max_depth); src_path = src_dir.filePath(relative_path); if (linkedPaths.contains(src_path)) { @@ -663,7 +663,7 @@ QString pathTruncate(const QString& path, int depth) QString trunc = QFileInfo(path).path(); - if (pathDepth(trunc) > depth ) { + if (pathDepth(trunc) > depth) { return pathTruncate(trunc, depth); } @@ -769,6 +769,9 @@ QString getDesktopDir() // Cross-platform Shortcut creation bool createShortcut(QString destination, QString target, QStringList args, QString name, QString icon) { + if (destination.isEmpty()) { + destination = PathCombine(getDesktopDir(), RemoveInvalidFilenameChars(name)); + } #if defined(Q_OS_MACOS) destination += ".command"; @@ -791,6 +794,8 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri return true; #elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) + if (!destination.endsWith(".desktop")) // in case of isFlatpak destination is already populated + destination += ".desktop"; QFile f(destination); f.open(QIODevice::WriteOnly | QIODevice::Text); QTextStream stream(&f); @@ -974,7 +979,7 @@ FilesystemType getFilesystemType(const QString& name) { for (auto iter = s_filesystem_type_names.constBegin(); iter != s_filesystem_type_names.constEnd(); ++iter) { auto fs_names = iter.value(); - if(fs_names.contains(name.toUpper())) + if (fs_names.contains(name.toUpper())) return iter.key(); } return FilesystemType::UNKNOWN; @@ -1151,7 +1156,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 +1385,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 4ac3b51ad..57a3143a1 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(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 066827829..b16a40ba6 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/MTPixmapCache.h b/launcher/MTPixmapCache.h index 57847a0e1..65cbe032a 100644 --- a/launcher/MTPixmapCache.h +++ b/launcher/MTPixmapCache.h @@ -3,6 +3,8 @@ #include #include #include +#include +#include #define GET_TYPE() \ Qt::ConnectionType type; \ @@ -60,6 +62,8 @@ class PixmapCache final : public QObject { DEFINE_FUNC_ONE_PARAM(remove, bool, const QPixmapCache::Key&) DEFINE_FUNC_TWO_PARAM(replace, bool, const QPixmapCache::Key&, const QPixmap&) DEFINE_FUNC_ONE_PARAM(setCacheLimit, bool, int) + DEFINE_FUNC_NO_PARAM(markCacheMissByEviciton, bool) + DEFINE_FUNC_ONE_PARAM(setFastEvictionThreshold, bool, int) // NOTE: Every function returns something non-void to simplify the macros. private slots: @@ -90,6 +94,43 @@ class PixmapCache final : public QObject { return true; } + /** + * Mark that a cache miss occurred because of a eviction if too many of these occur too fast the cache size is increased + * @return if the cache size was increased + */ + bool _markCacheMissByEviciton() + { + auto now = QTime::currentTime(); + if (!m_last_cache_miss_by_eviciton.isNull()) { + auto diff = m_last_cache_miss_by_eviciton.msecsTo(now); + if (diff < 1000) { // less than a second ago + ++m_consecutive_fast_evicitons; + } else { + m_consecutive_fast_evicitons = 0; + } + } + m_last_cache_miss_by_eviciton = now; + if (m_consecutive_fast_evicitons >= m_consecutive_fast_evicitons_threshold) { + // double the cache size + auto newSize = _cacheLimit() * 2; + qDebug() << m_consecutive_fast_evicitons << "pixmap cache misses by eviction happened too fast, doubling cache size to" + << newSize; + _setCacheLimit(newSize); + m_consecutive_fast_evicitons = 0; + return true; + } + return false; + } + + bool _setFastEvictionThreshold(int threshold) + { + m_consecutive_fast_evicitons_threshold = threshold; + return true; + } + private: static PixmapCache* s_instance; + QTime m_last_cache_miss_by_eviciton; + int m_consecutive_fast_evicitons = 0; + int m_consecutive_fast_evicitons_threshold = 15; }; diff --git a/launcher/Markdown.cpp b/launcher/Markdown.cpp new file mode 100644 index 000000000..426067bf6 --- /dev/null +++ b/launcher/Markdown.cpp @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 Joshua Goins + * + * 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 . + */ + +#include "Markdown.h" + +QString markdownToHTML(const QString& markdown) +{ + const QByteArray markdownData = markdown.toUtf8(); + char* buffer = cmark_markdown_to_html(markdownData.constData(), markdownData.length(), CMARK_OPT_NOBREAKS | CMARK_OPT_UNSAFE); + + QString htmlStr(buffer); + + free(buffer); + + return htmlStr; +} \ No newline at end of file diff --git a/launcher/Markdown.h b/launcher/Markdown.h index f115dd570..6b261e60c 100644 --- a/launcher/Markdown.h +++ b/launcher/Markdown.h @@ -21,14 +21,4 @@ #include #include -static QString markdownToHTML(const QString& markdown) -{ - const QByteArray markdownData = markdown.toUtf8(); - char* buffer = cmark_markdown_to_html(markdownData.constData(), markdownData.length(), CMARK_OPT_NOBREAKS | CMARK_OPT_UNSAFE); - - QString htmlStr(buffer); - - free(buffer); - - return htmlStr; -} \ No newline at end of file +QString markdownToHTML(const QString& markdown); \ No newline at end of file diff --git a/launcher/ResourceDownloadTask.cpp b/launcher/ResourceDownloadTask.cpp index 61b918aaf..06c03c779 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 -* Copyright (C) 2022 Sefa Eyeoglu -* -* 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 . -*/ +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022-2023 flowln + * Copyright (C) 2022 Sefa Eyeoglu + * + * 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 . + */ #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 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(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 73ad2d070..2baddf8a8 100644 --- a/launcher/ResourceDownloadTask.h +++ b/launcher/ResourceDownloadTask.h @@ -1,44 +1,53 @@ // SPDX-License-Identifier: GPL-3.0-only /* -* Prism Launcher - Minecraft Launcher -* Copyright (c) 2022-2023 flowln -* Copyright (C) 2022 Sefa Eyeoglu -* -* 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 . -*/ + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022-2023 flowln + * Copyright (C) 2022 Sefa Eyeoglu + * + * 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 . + */ #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 packs, bool is_indexed = true); + public: + explicit ResourceDownloadTask(ModPlatform::IndexedPack::Ptr pack, + ModPlatform::IndexedVersion version, + const std::shared_ptr packs, + bool is_indexed = true, + QString custom_target_folder = {}); const QString& getFilename() const { return m_pack_version.fileName; } - const QString& getCustomPath() const { return m_pack_version.custom_target_folder; } + const QString& getCustomPath() const { return m_custom_target_folder; } const QVariant& getVersionID() const { return m_pack_version.fileId; } + const ModPlatform::IndexedVersion& getVersion() const { return m_pack_version; } + const ModPlatform::ResourceProvider& getProvider() const { return m_pack->provider; } + const QString& getName() const { return m_pack->name; } + ModPlatform::IndexedPack::Ptr getPack() { return m_pack; } -private: - ModPlatform::IndexedPack m_pack; + private: + ModPlatform::IndexedPack::Ptr m_pack; ModPlatform::IndexedVersion m_pack_version; const std::shared_ptr m_pack_model; + QString m_custom_target_folder; NetJob::Ptr m_filesNetJob; LocalModUpdateTask::Ptr m_update_task; @@ -47,11 +56,8 @@ private: void downloadFailed(QString reason); void downloadSucceeded(); - std::tuple to_delete {"", ""}; + std::tuple to_delete{ "", "" }; -private slots: + private slots: void hasOldResource(QString name, QString filename); }; - - - diff --git a/launcher/VersionProxyModel.cpp b/launcher/VersionProxyModel.cpp index 6aba268d8..e5c665662 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 + * Copyright (C) 2023 TheKodeToad * * 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 8991c31b0..6434376c6 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 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 b4c55b3d7..e4a686c27 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::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 b29af8576..3407fdf72 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 + * Copyright (C) 2023 TheKodeToad * * 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(m_vlist[index.row()]); switch (role) { + case SortRole: + return -index.row(); case VersionPointerRole: return QVariant::fromValue(m_vlist[index.row()]); case VersionIdRole: diff --git a/launcher/launch/steps/CheckJava.cpp b/launcher/launch/steps/CheckJava.cpp index f01875861..7d697ba96 100644 --- a/launcher/launch/steps/CheckJava.cpp +++ b/launcher/launch/steps/CheckJava.cpp @@ -81,15 +81,20 @@ void CheckJava::executeTask() } QFileInfo javaInfo(realJavaPath); - qlonglong javaUnixTime = javaInfo.lastModified().toMSecsSinceEpoch(); - auto storedUnixTime = settings->get("JavaTimestamp").toLongLong(); + qint64 javaUnixTime = javaInfo.lastModified().toMSecsSinceEpoch(); + auto storedSignature = settings->get("JavaSignature").toString(); auto storedArchitecture = settings->get("JavaArchitecture").toString(); auto storedRealArchitecture = settings->get("JavaRealArchitecture").toString(); auto storedVersion = settings->get("JavaVersion").toString(); auto storedVendor = settings->get("JavaVendor").toString(); - m_javaUnixTime = javaUnixTime; + + QCryptographicHash hash(QCryptographicHash::Sha1); + hash.addData(QByteArray::number(javaUnixTime)); + hash.addData(m_javaPath.toUtf8()); + m_javaSignature = hash.result().toHex(); + // if timestamps are not the same, or something is missing, check! - if (javaUnixTime != storedUnixTime || storedVersion.size() == 0 + if (m_javaSignature != storedSignature || storedVersion.size() == 0 || storedArchitecture.size() == 0 || storedRealArchitecture.size() == 0 || storedVendor.size() == 0) { @@ -140,7 +145,7 @@ void CheckJava::checkJavaFinished(JavaCheckResult result) instance->settings()->set("JavaArchitecture", result.mojangPlatform); instance->settings()->set("JavaRealArchitecture", result.realPlatform); instance->settings()->set("JavaVendor", result.javaVendor); - instance->settings()->set("JavaTimestamp", m_javaUnixTime); + instance->settings()->set("JavaSignature", m_javaSignature); emitSucceeded(); return; } diff --git a/launcher/launch/steps/CheckJava.h b/launcher/launch/steps/CheckJava.h index d084b1321..bbf06b7c7 100644 --- a/launcher/launch/steps/CheckJava.h +++ b/launcher/launch/steps/CheckJava.h @@ -40,6 +40,6 @@ private: private: QString m_javaPath; - qlonglong m_javaUnixTime; + QString m_javaSignature; JavaCheckerPtr m_JavaChecker; }; diff --git a/launcher/meta/JsonFormat.cpp b/launcher/meta/JsonFormat.cpp index 473f37d66..cb2d06ea0 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 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 e617abf82..0718a4204 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 781561931..59a96a68b 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 7f001dfc2..9f4482784 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 7e5b60589..ff81fcbb8 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 2c624a365..aab930de0 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -148,10 +148,11 @@ void MinecraftInstance::loadSpecificSettings() m_settings->registerOverride(global_settings->getSetting("IgnoreJavaCompatibility"), javaOrLocation); // special! - m_settings->registerPassthrough(global_settings->getSetting("JavaTimestamp"), javaOrLocation); - m_settings->registerPassthrough(global_settings->getSetting("JavaVersion"), javaOrLocation); + m_settings->registerPassthrough(global_settings->getSetting("JavaSignature"), javaOrLocation); m_settings->registerPassthrough(global_settings->getSetting("JavaArchitecture"), javaOrLocation); m_settings->registerPassthrough(global_settings->getSetting("JavaRealArchitecture"), javaOrLocation); + m_settings->registerPassthrough(global_settings->getSetting("JavaVersion"), javaOrLocation); + m_settings->registerPassthrough(global_settings->getSetting("JavaVendor"), javaOrLocation); // Window Size auto windowSetting = m_settings->registerSetting("OverrideWindow", false); diff --git a/launcher/minecraft/OneSixVersionFormat.cpp b/launcher/minecraft/OneSixVersionFormat.cpp index 888b68609..b586198bf 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 11c5a3af3..8e9dd1670 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 9e2fd1113..c27941472 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 5c58f6b27..ca75cd2aa 100644 --- a/launcher/minecraft/mod/DataPack.cpp +++ b/launcher/minecraft/mod/DataPack.cpp @@ -33,7 +33,9 @@ static const QMap> s_pack_format_versions = { { 4, { Version("1.13"), Version("1.14.4") } }, { 5, { Version("1.15"), Version("1.16.1") } }, { 6, { Version("1.16.2"), Version("1.16.5") } }, { 7, { Version("1.17"), Version("1.17.1") } }, { 8, { Version("1.18"), Version("1.18.1") } }, { 9, { Version("1.18.2"), Version("1.18.2") } }, - { 10, { Version("1.19"), Version("1.19.3") } }, + { 10, { Version("1.19"), Version("1.19.3") } }, { 11, { Version("23w03a"), Version("23w05a") } }, + { 12, { Version("1.19.4"), Version("1.19.4") } }, { 13, { Version("23w12a"), Version("23w14a") } }, + { 14, { Version("23w16a"), Version("23w17a") } }, { 15, { Version("1.20"), Version("1.20") } }, }; void DataPack::setPackFormat(int new_format_id) diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp index c495cd47e..e613ddeb7 100644 --- a/launcher/minecraft/mod/Mod.cpp +++ b/launcher/minecraft/mod/Mod.cpp @@ -41,9 +41,11 @@ #include #include +#include "MTPixmapCache.h" #include "MetadataHandler.h" #include "Version.h" #include "minecraft/mod/ModDetails.h" +#include "minecraft/mod/tasks/LocalModParseTask.h" static ModPlatform::ProviderCapabilities ProviderCaps; @@ -201,7 +203,10 @@ void Mod::finishResolvingWithDetails(ModDetails&& details) m_local_details = std::move(details); if (metadata) setMetadata(std::move(metadata)); -}; + if (!iconPath().isEmpty()) { + m_pack_image_cache_key.was_read_attempt = false; + } +} auto Mod::provider() const -> std::optional { @@ -210,6 +215,56 @@ auto Mod::provider() const -> std::optional return {}; } +auto Mod::licenses() const -> const QList& +{ + return details().licenses; +} + + auto Mod::issueTracker() const -> QString +{ + return details().issue_tracker; +} + +void Mod::setIcon(QImage new_image) const +{ + QMutexLocker locker(&m_data_lock); + + Q_ASSERT(!new_image.isNull()); + + if (m_pack_image_cache_key.key.isValid()) + PixmapCache::remove(m_pack_image_cache_key.key); + + // scale the image to avoid flooding the pixmapcache + auto pixmap = QPixmap::fromImage(new_image.scaled({64, 64}, Qt::AspectRatioMode::KeepAspectRatioByExpanding)); + + m_pack_image_cache_key.key = PixmapCache::insert(pixmap); + m_pack_image_cache_key.was_ever_used = true; + m_pack_image_cache_key.was_read_attempt = true; +} + +QPixmap Mod::icon(QSize size, Qt::AspectRatioMode mode) const +{ + QPixmap cached_image; + if (PixmapCache::find(m_pack_image_cache_key.key, &cached_image)) { + if (size.isNull()) + return cached_image; + return cached_image.scaled(size, mode); + } + + // No valid image we can get + if ((!m_pack_image_cache_key.was_ever_used && m_pack_image_cache_key.was_read_attempt) || iconPath().isEmpty()) + return {}; + + if (m_pack_image_cache_key.was_ever_used) { + qDebug() << "Mod" << name() << "Had it's icon evicted form the cache. reloading..."; + PixmapCache::markCacheMissByEviciton(); + } + // Image got evicted from the cache or an attempt to load it has not been made. load it and retry. + m_pack_image_cache_key.was_read_attempt = true; + ModUtils::loadIconFile(*this); + return icon(size); +} + bool Mod::valid() const { return !m_local_details.mod_id.isEmpty(); diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h index c40325387..d4e419f4f 100644 --- a/launcher/minecraft/mod/Mod.h +++ b/launcher/minecraft/mod/Mod.h @@ -38,6 +38,10 @@ #include #include #include +#include +#include +#include +#include #include @@ -64,6 +68,15 @@ public: auto authors() const -> QStringList; auto status() const -> ModStatus; auto provider() const -> std::optional; + auto licenses() const -> const QList&; + auto issueTracker() const -> QString; + + /** Get the intneral path to the mod's icon file*/ + QString iconPath() const { return m_local_details.icon_file; }; + /** Gets the icon of the mod, converted to a QPixmap for drawing, and scaled to size. */ + [[nodiscard]] QPixmap icon(QSize size, Qt::AspectRatioMode mode = Qt::AspectRatioMode::IgnoreAspectRatio) const; + /** Thread-safe. */ + void setIcon(QImage new_image) const; auto metadata() -> std::shared_ptr; auto metadata() const -> const std::shared_ptr; @@ -85,4 +98,13 @@ public: protected: ModDetails m_local_details; + + mutable QMutex m_data_lock; + + struct { + QPixmapCache::Key key; + bool was_ever_used = false; + bool was_read_attempt = false; + } mutable m_pack_image_cache_key; + }; diff --git a/launcher/minecraft/mod/ModDetails.h b/launcher/minecraft/mod/ModDetails.h index 176e4fc14..b4e59d52d 100644 --- a/launcher/minecraft/mod/ModDetails.h +++ b/launcher/minecraft/mod/ModDetails.h @@ -39,6 +39,7 @@ #include #include +#include #include "minecraft/mod/MetadataHandler.h" @@ -49,6 +50,84 @@ enum class ModStatus { Unknown, // Default status }; +struct ModLicense { + QString name = {}; + QString id = {}; + QString url = {}; + QString description = {}; + + ModLicense() {} + + ModLicense(const QString license) { + // FIXME: come up with a better license parseing. + // handle SPDX identifiers? https://spdx.org/licenses/ + auto parts = license.split(' '); + QStringList notNameParts = {}; + for (auto part : parts) { + auto url = QUrl(part); + if (part.startsWith("(") && part.endsWith(")")) + url = QUrl(part.mid(1, part.size() - 2)); + + if (url.isValid() && !url.scheme().isEmpty() && !url.host().isEmpty()) { + this->url = url.toString(); + notNameParts.append(part); + continue; + } + } + + for (auto part : notNameParts) { + parts.removeOne(part); + } + + auto licensePart = parts.join(' '); + this->name = licensePart; + this->description = licensePart; + + if (parts.size() == 1) { + this->id = parts.first(); + } + + } + + ModLicense(const QString name, const QString id, const QString url, const QString description) { + this->name = name; + this->id = id; + this->url = url; + this->description = description; + } + + ModLicense(const ModLicense& other) + : name(other.name) + , id(other.id) + , url(other.url) + , description(other.description) + {} + + ModLicense& operator=(const ModLicense& other) + { + this->name = other.name; + this->id = other.id; + this->url = other.url; + this->description = other.description; + + return *this; + } + + ModLicense& operator=(const ModLicense&& other) + { + this->name = other.name; + this->id = other.id; + this->url = other.url; + this->description = other.description; + + return *this; + } + + bool isEmpty() { + return this->name.isEmpty() && this->id.isEmpty() && this->url.isEmpty() && this->description.isEmpty(); + } +}; + struct ModDetails { /* Mod ID as defined in the ModLoader-specific metadata */ @@ -72,6 +151,15 @@ struct ModDetails /* List of the author's names */ QStringList authors = {}; + /* Issue Tracker URL */ + QString issue_tracker = {}; + + /* License */ + QList licenses = {}; + + /* Path of mod logo */ + QString icon_file = {}; + /* Installation status of the mod */ ModStatus status = ModStatus::Unknown; @@ -89,6 +177,9 @@ struct ModDetails , homeurl(other.homeurl) , description(other.description) , authors(other.authors) + , issue_tracker(other.issue_tracker) + , licenses(other.licenses) + , icon_file(other.icon_file) , status(other.status) {} @@ -101,6 +192,9 @@ struct ModDetails this->homeurl = other.homeurl; this->description = other.description; this->authors = other.authors; + this->issue_tracker = other.issue_tracker; + this->licenses = other.licenses; + this->icon_file = other.icon_file; this->status = other.status; return *this; @@ -115,6 +209,9 @@ struct ModDetails this->homeurl = other.homeurl; this->description = other.description; this->authors = other.authors; + this->issue_tracker = other.issue_tracker; + this->licenses = other.licenses; + this->icon_file = other.icon_file; this->status = other.status; return *this; diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index 5e3b31e08..af98d8348 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -37,6 +37,7 @@ #include "ModFolderModel.h" #include +#include #include #include #include @@ -52,12 +53,14 @@ #include "minecraft/mod/tasks/LocalModParseTask.h" #include "minecraft/mod/tasks/ModFolderLoadTask.h" -#include "modplatform/ModIndex.h" ModFolderModel::ModFolderModel(const QString& dir, BaseInstance* instance, bool is_indexed, bool create_dir) : ResourceFolderModel(QDir(dir), instance, nullptr, create_dir), m_is_indexed(is_indexed) { - m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::VERSION, SortType::DATE, SortType::PROVIDER }; + m_column_names = QStringList({ "Enable", "Image", "Name", "Version", "Last Modified", "Provider" }); + m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Version"), tr("Last Modified"), tr("Provider") }); + m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME , SortType::VERSION, SortType::DATE, SortType::PROVIDER}; + m_column_resize_modes = { QHeaderView::ResizeToContents, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::ResizeToContents, QHeaderView::ResizeToContents, QHeaderView::ResizeToContents}; } QVariant ModFolderModel::data(const QModelIndex &index, int role) const @@ -118,7 +121,9 @@ QVariant ModFolderModel::data(const QModelIndex &index, int role) const case Qt::DecorationRole: { if (column == NAME_COLUMN && (at(row)->isSymLinkUnder(instDirPath()) || at(row)->isMoreThanOneHardLink())) return APPLICATION->getThemedIcon("status-yellow"); - + if (column == ImageColumn) { + return at(row)->icon({32, 32}, Qt::AspectRatioMode::KeepAspectRatioByExpanding); + } return {}; } case Qt::CheckStateRole: @@ -142,15 +147,12 @@ QVariant ModFolderModel::headerData(int section, Qt::Orientation orientation, in switch (section) { case ActiveColumn: - return QString(); case NameColumn: - return tr("Name"); case VersionColumn: - return tr("Version"); case DateColumn: - return tr("Last changed"); case ProviderColumn: - return tr("Provider"); + case ImageColumn: + return columnNames().at(section); default: return QVariant(); } diff --git a/launcher/minecraft/mod/ModFolderModel.h b/launcher/minecraft/mod/ModFolderModel.h index d337fe296..6ccaba235 100644 --- a/launcher/minecraft/mod/ModFolderModel.h +++ b/launcher/minecraft/mod/ModFolderModel.h @@ -64,6 +64,7 @@ public: enum Columns { ActiveColumn = 0, + ImageColumn, NameColumn, VersionColumn, DateColumn, @@ -77,6 +78,8 @@ public: }; ModFolderModel(const QString &dir, BaseInstance* instance, bool is_indexed = false, bool create_dir = true); + virtual QString id() const override { return "mods"; } + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; diff --git a/launcher/minecraft/mod/ResourceFolderModel.cpp b/launcher/minecraft/mod/ResourceFolderModel.cpp index d2d875e48..7700fd36b 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.cpp +++ b/launcher/minecraft/mod/ResourceFolderModel.cpp @@ -8,12 +8,15 @@ #include #include #include +#include #include "Application.h" #include "FileSystem.h" +#include "QVariantUtils.h" #include "minecraft/mod/tasks/BasicFolderLoadTask.h" +#include "settings/Setting.h" #include "tasks/Task.h" ResourceFolderModel::ResourceFolderModel(QDir dir, BaseInstance* instance, QObject* parent, bool create_dir) @@ -471,10 +474,10 @@ QVariant ResourceFolderModel::headerData(int section, Qt::Orientation orientatio switch (role) { case Qt::DisplayRole: switch (section) { + case ACTIVE_COLUMN: case NAME_COLUMN: - return tr("Name"); case DATE_COLUMN: - return tr("Last modified"); + return columnNames().at(section); default: return {}; } @@ -500,6 +503,75 @@ QVariant ResourceFolderModel::headerData(int section, Qt::Orientation orientatio return {}; } +void ResourceFolderModel::setupHeaderAction(QAction* act, int column) +{ + Q_ASSERT(act); + + act->setText(columnNames().at(column)); +} + +void ResourceFolderModel::saveHiddenColumn(int column, bool hidden) +{ + auto const setting_name = QString("UI/%1_Page/HiddenColumns").arg(id()); + auto setting = (m_instance->settings()->contains(setting_name)) ? + m_instance->settings()->getSetting(setting_name) : m_instance->settings()->registerSetting(setting_name); + + auto hiddenColumns = setting->get().toStringList(); + auto name = columnNames(false).at(column); + auto index = hiddenColumns.indexOf(name); + if (index >= 0 && !hidden) { + hiddenColumns.removeAt(index); + } else if ( index < 0 && hidden) { + hiddenColumns.append(name); + } + setting->set(hiddenColumns); +} + +void ResourceFolderModel::loadHiddenColumns(QTreeView *tree) +{ + auto const setting_name = QString("UI/%1_Page/HiddenColumns").arg(id()); + auto setting = (m_instance->settings()->contains(setting_name)) ? + m_instance->settings()->getSetting(setting_name) : m_instance->settings()->registerSetting(setting_name); + + auto hiddenColumns = setting->get().toStringList(); + auto col_names = columnNames(false); + for (auto col_name : hiddenColumns) { + auto index = col_names.indexOf(col_name); + if (index >= 0) + tree->setColumnHidden(index, true); + } + +} + +QMenu* ResourceFolderModel::createHeaderContextMenu(QTreeView* tree) +{ + auto menu = new QMenu(tree); + + menu->addSeparator()->setText(tr("Show / Hide Columns")); + + for (int col = 0; col < columnCount(); ++col) { + auto act = new QAction(menu); + setupHeaderAction(act, col); + + act->setCheckable(true); + act->setChecked(!tree->isColumnHidden(col)); + + connect(act, &QAction::toggled, tree, [this, col, tree](bool toggled){ + tree->setColumnHidden(col, !toggled); + for(int c = 0; c < columnCount(); ++c) { + if (m_column_resize_modes.at(c) == QHeaderView::ResizeToContents) + tree->resizeColumnToContents(c); + } + saveHiddenColumn(col, !toggled); + }); + + menu->addAction(act); + + } + + return menu; +} + QSortFilterProxyModel* ResourceFolderModel::createFilterProxyModel(QObject* parent) { return new ProxyModel(parent); diff --git a/launcher/minecraft/mod/ResourceFolderModel.h b/launcher/minecraft/mod/ResourceFolderModel.h index 7b5256e03..016679dd1 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.h +++ b/launcher/minecraft/mod/ResourceFolderModel.h @@ -1,5 +1,8 @@ #pragma once +#include +#include +#include #include #include #include @@ -29,6 +32,8 @@ class ResourceFolderModel : public QAbstractListModel { ResourceFolderModel(QDir, BaseInstance* instance, QObject* parent = nullptr, bool create_dir = true); ~ResourceFolderModel() override; + virtual QString id() const { return "resource"; } + /** Starts watching the paths for changes. * * Returns whether starting to watch all the paths was successful. @@ -92,6 +97,7 @@ class ResourceFolderModel : public QAbstractListModel { /* Basic columns */ enum Columns { ACTIVE_COLUMN = 0, NAME_COLUMN, DATE_COLUMN, NUM_COLUMNS }; + QStringList columnNames(bool translated = true) const { return translated ? m_column_names_translated : m_column_names; }; [[nodiscard]] int rowCount(const QModelIndex& parent = {}) const override { return parent.isValid() ? 0 : static_cast(size()); } [[nodiscard]] int columnCount(const QModelIndex& parent = {}) const override { return parent.isValid() ? 0 : NUM_COLUMNS; } @@ -110,6 +116,11 @@ class ResourceFolderModel : public QAbstractListModel { [[nodiscard]] QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + void setupHeaderAction(QAction* act, int column); + void saveHiddenColumn(int column, bool hidden); + void loadHiddenColumns(QTreeView* tree); + QMenu* createHeaderContextMenu(QTreeView* tree); + /** This creates a proxy model to filter / sort the model for a UI. * * The actual comparisons and filtering are done directly by the Resource, so to modify behavior go there instead! @@ -117,6 +128,7 @@ class ResourceFolderModel : public QAbstractListModel { QSortFilterProxyModel* createFilterProxyModel(QObject* parent = nullptr); [[nodiscard]] SortType columnToSortKey(size_t column) const; + [[nodiscard]] QList columnResizeModes() const { return m_column_resize_modes; } class ProxyModel : public QSortFilterProxyModel { public: @@ -187,6 +199,9 @@ class ResourceFolderModel : public QAbstractListModel { // Represents the relationship between a column's index (represented by the list index), and it's sorting key. // As such, the order in with they appear is very important! QList m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::DATE }; + QStringList m_column_names = {"Enable", "Name", "Last Modified"}; + QStringList m_column_names_translated = {tr("Enable"), tr("Name"), tr("Last Modified")}; + QList m_column_resize_modes = { QHeaderView::ResizeToContents, QHeaderView::Stretch, QHeaderView::ResizeToContents }; bool m_can_interact = true; diff --git a/launcher/minecraft/mod/ResourcePack.cpp b/launcher/minecraft/mod/ResourcePack.cpp index 876d5c3ee..e06c1ac1a 100644 --- a/launcher/minecraft/mod/ResourcePack.cpp +++ b/launcher/minecraft/mod/ResourcePack.cpp @@ -18,7 +18,8 @@ static const QMap> s_pack_format_versions = { { 5, { Version("1.15"), Version("1.16.1") } }, { 6, { Version("1.16.2"), Version("1.16.5") } }, { 7, { Version("1.17"), Version("1.17.1") } }, { 8, { Version("1.18"), Version("1.18.2") } }, { 9, { Version("1.19"), Version("1.19.2") } }, { 11, { Version("22w42a"), Version("22w44a") } }, - { 12, { Version("1.19.3"), Version("1.19.3") } }, + { 12, { Version("1.19.3"), Version("1.19.3") } }, { 13, { Version("1.19.4"), Version("1.19.4") } }, + { 14, { Version("1.20"), Version("1.20") } } }; void ResourcePack::setPackFormat(int new_format_id) @@ -39,7 +40,7 @@ void ResourcePack::setDescription(QString new_description) m_description = new_description; } -void ResourcePack::setImage(QImage new_image) +void ResourcePack::setImage(QImage new_image) const { QMutexLocker locker(&m_data_lock); @@ -48,7 +49,10 @@ void ResourcePack::setImage(QImage new_image) if (m_pack_image_cache_key.key.isValid()) PixmapCache::instance().remove(m_pack_image_cache_key.key); - m_pack_image_cache_key.key = PixmapCache::instance().insert(QPixmap::fromImage(new_image)); + // scale the image to avoid flooding the pixmapcache + auto pixmap = QPixmap::fromImage(new_image.scaled({64, 64}, Qt::AspectRatioMode::KeepAspectRatioByExpanding)); + + m_pack_image_cache_key.key = PixmapCache::instance().insert(pixmap); m_pack_image_cache_key.was_ever_used = true; // This can happen if the pixmap is too big to fit in the cache :c @@ -58,21 +62,25 @@ void ResourcePack::setImage(QImage new_image) } } -QPixmap ResourcePack::image(QSize size) +QPixmap ResourcePack::image(QSize size, Qt::AspectRatioMode mode) const { QPixmap cached_image; if (PixmapCache::instance().find(m_pack_image_cache_key.key, &cached_image)) { if (size.isNull()) return cached_image; - return cached_image.scaled(size); + return cached_image.scaled(size, mode); } // No valid image we can get - if (!m_pack_image_cache_key.was_ever_used) + if (!m_pack_image_cache_key.was_ever_used) { return {}; + } else { + qDebug() << "Resource Pack" << name() << "Had it's image evicted from the cache. reloading..."; + PixmapCache::markCacheMissByEviciton(); + } // Imaged got evicted from the cache. Re-process it and retry. - ResourcePackUtils::process(*this); + ResourcePackUtils::processPackPNG(*this); return image(size); } diff --git a/launcher/minecraft/mod/ResourcePack.h b/launcher/minecraft/mod/ResourcePack.h index 7cb414d83..da354bc1c 100644 --- a/launcher/minecraft/mod/ResourcePack.h +++ b/launcher/minecraft/mod/ResourcePack.h @@ -31,7 +31,7 @@ class ResourcePack : public Resource { [[nodiscard]] QString description() const { return m_description; } /** Gets the image of the resource pack, converted to a QPixmap for drawing, and scaled to size. */ - [[nodiscard]] QPixmap image(QSize size); + [[nodiscard]] QPixmap image(QSize size, Qt::AspectRatioMode mode = Qt::AspectRatioMode::IgnoreAspectRatio) const; /** Thread-safe. */ void setPackFormat(int new_format_id); @@ -40,7 +40,7 @@ class ResourcePack : public Resource { void setDescription(QString new_description); /** Thread-safe. */ - void setImage(QImage new_image); + void setImage(QImage new_image) const; bool valid() const override; @@ -67,5 +67,5 @@ class ResourcePack : public Resource { struct { QPixmapCache::Key key; bool was_ever_used = false; - } m_pack_image_cache_key; + } mutable m_pack_image_cache_key; }; diff --git a/launcher/minecraft/mod/ResourcePackFolderModel.cpp b/launcher/minecraft/mod/ResourcePackFolderModel.cpp index c12d1f237..41455599b 100644 --- a/launcher/minecraft/mod/ResourcePackFolderModel.cpp +++ b/launcher/minecraft/mod/ResourcePackFolderModel.cpp @@ -35,6 +35,8 @@ */ #include "ResourcePackFolderModel.h" +#include +#include #include #include @@ -48,7 +50,11 @@ ResourcePackFolderModel::ResourcePackFolderModel(const QString& dir, BaseInstance* instance) : ResourceFolderModel(QDir(dir), instance) { - m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::PACK_FORMAT, SortType::DATE }; + m_column_names = QStringList({ "Enable", "Image", "Name", "Pack Format", "Last Modified" }); + m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Pack Format"), tr("Last Modified") }); + m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::PACK_FORMAT, SortType::DATE}; + m_column_resize_modes = { QHeaderView::ResizeToContents, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::ResizeToContents, QHeaderView::ResizeToContents }; + } QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const @@ -84,9 +90,11 @@ QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const return {}; } case Qt::DecorationRole: { - if (column == NAME_COLUMN && (at(row)->isSymLinkUnder(instDirPath()) || at(row)->isMoreThanOneHardLink())) + if (column == NameColumn && (at(row)->isSymLinkUnder(instDirPath()) || at(row)->isMoreThanOneHardLink())) return APPLICATION->getThemedIcon("status-yellow"); - + if (column == ImageColumn) { + return at(row)->image({32, 32}, Qt::AspectRatioMode::KeepAspectRatioByExpanding); + } return {}; } case Qt::ToolTipRole: { @@ -94,7 +102,7 @@ QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const //: The string being explained by this is in the format: ID (Lower version - Upper version) return tr("The resource pack format ID, as well as the Minecraft versions it was designed for."); } - if (column == NAME_COLUMN) { + if (column == NameColumn) { if (at(row)->isSymLinkUnder(instDirPath())) { return m_resources[row]->internal_id() + tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original." @@ -126,13 +134,11 @@ QVariant ResourcePackFolderModel::headerData(int section, Qt::Orientation orient case Qt::DisplayRole: switch (section) { case ActiveColumn: - return QString(); case NameColumn: - return tr("Name"); case PackFormatColumn: - return tr("Pack Format"); case DateColumn: - return tr("Last changed"); + case ImageColumn: + return columnNames().at(section); default: return {}; } @@ -151,6 +157,11 @@ QVariant ResourcePackFolderModel::headerData(int section, Qt::Orientation orient default: return {}; } + case Qt::SizeHintRole: + if (section == ImageColumn) { + return QSize(64,0); + } + return {}; default: return {}; } diff --git a/launcher/minecraft/mod/ResourcePackFolderModel.h b/launcher/minecraft/mod/ResourcePackFolderModel.h index db4b14fb0..531d81928 100644 --- a/launcher/minecraft/mod/ResourcePackFolderModel.h +++ b/launcher/minecraft/mod/ResourcePackFolderModel.h @@ -11,6 +11,7 @@ public: enum Columns { ActiveColumn = 0, + ImageColumn, NameColumn, PackFormatColumn, DateColumn, @@ -19,6 +20,8 @@ public: explicit ResourcePackFolderModel(const QString &dir, BaseInstance* instance); + virtual QString id() const override { return "resourcepacks"; } + [[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; [[nodiscard]] QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; diff --git a/launcher/minecraft/mod/ShaderPackFolderModel.h b/launcher/minecraft/mod/ShaderPackFolderModel.h index dc5acf80f..f8249962f 100644 --- a/launcher/minecraft/mod/ShaderPackFolderModel.h +++ b/launcher/minecraft/mod/ShaderPackFolderModel.h @@ -9,4 +9,6 @@ class ShaderPackFolderModel : public ResourceFolderModel { explicit ShaderPackFolderModel(const QString& dir, BaseInstance* instance) : ResourceFolderModel(QDir(dir), instance) {} + + virtual QString id() const override { return "shaderpacks"; } }; diff --git a/launcher/minecraft/mod/TexturePack.cpp b/launcher/minecraft/mod/TexturePack.cpp index 99d555843..c7a50a97a 100644 --- a/launcher/minecraft/mod/TexturePack.cpp +++ b/launcher/minecraft/mod/TexturePack.cpp @@ -23,6 +23,8 @@ #include #include +#include "MTPixmapCache.h" + #include "minecraft/mod/tasks/LocalTexturePackParseTask.h" void TexturePack::setDescription(QString new_description) @@ -32,34 +34,41 @@ void TexturePack::setDescription(QString new_description) m_description = new_description; } -void TexturePack::setImage(QImage new_image) +void TexturePack::setImage(QImage new_image) const { QMutexLocker locker(&m_data_lock); Q_ASSERT(!new_image.isNull()); if (m_pack_image_cache_key.key.isValid()) - QPixmapCache::remove(m_pack_image_cache_key.key); + PixmapCache::remove(m_pack_image_cache_key.key); - m_pack_image_cache_key.key = QPixmapCache::insert(QPixmap::fromImage(new_image)); + // scale the image to avoid flooding the pixmapcache + auto pixmap = QPixmap::fromImage(new_image.scaled({64, 64}, Qt::AspectRatioMode::KeepAspectRatioByExpanding)); + + m_pack_image_cache_key.key = PixmapCache::insert(pixmap); m_pack_image_cache_key.was_ever_used = true; } -QPixmap TexturePack::image(QSize size) +QPixmap TexturePack::image(QSize size, Qt::AspectRatioMode mode) const { QPixmap cached_image; - if (QPixmapCache::find(m_pack_image_cache_key.key, &cached_image)) { + if (PixmapCache::find(m_pack_image_cache_key.key, &cached_image)) { if (size.isNull()) return cached_image; - return cached_image.scaled(size); + return cached_image.scaled(size, mode); } // No valid image we can get - if (!m_pack_image_cache_key.was_ever_used) + if (!m_pack_image_cache_key.was_ever_used) { return {}; + } else { + qDebug() << "Texture Pack" << name() << "Had it's image evicted from the cache. reloading..."; + PixmapCache::markCacheMissByEviciton(); + } // Imaged got evicted from the cache. Re-process it and retry. - TexturePackUtils::process(*this); + TexturePackUtils::processPackPNG(*this); return image(size); } diff --git a/launcher/minecraft/mod/TexturePack.h b/launcher/minecraft/mod/TexturePack.h index 81bd5c699..577005655 100644 --- a/launcher/minecraft/mod/TexturePack.h +++ b/launcher/minecraft/mod/TexturePack.h @@ -40,13 +40,13 @@ class TexturePack : public Resource { [[nodiscard]] QString description() const { return m_description; } /** Gets the image of the texture pack, converted to a QPixmap for drawing, and scaled to size. */ - [[nodiscard]] QPixmap image(QSize size); + [[nodiscard]] QPixmap image(QSize size, Qt::AspectRatioMode mode = Qt::AspectRatioMode::IgnoreAspectRatio) const; /** Thread-safe. */ void setDescription(QString new_description); /** Thread-safe. */ - void setImage(QImage new_image); + void setImage(QImage new_image) const; bool valid() const override; @@ -65,5 +65,5 @@ class TexturePack : public Resource { struct { QPixmapCache::Key key; bool was_ever_used = false; - } m_pack_image_cache_key; + } mutable m_pack_image_cache_key; }; diff --git a/launcher/minecraft/mod/TexturePackFolderModel.cpp b/launcher/minecraft/mod/TexturePackFolderModel.cpp index c6609ed1e..531a70232 100644 --- a/launcher/minecraft/mod/TexturePackFolderModel.cpp +++ b/launcher/minecraft/mod/TexturePackFolderModel.cpp @@ -33,6 +33,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +#include + +#include "Application.h" #include "TexturePackFolderModel.h" @@ -41,7 +44,13 @@ TexturePackFolderModel::TexturePackFolderModel(const QString& dir, BaseInstance* instance) : ResourceFolderModel(QDir(dir), instance) -{} +{ + m_column_names = QStringList({ "Enable", "Image", "Name", "Last Modified" }); + m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Last Modified") }); + m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::DATE }; + m_column_resize_modes = { QHeaderView::ResizeToContents, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::ResizeToContents}; + +} Task* TexturePackFolderModel::createUpdateTask() { @@ -52,3 +61,96 @@ Task* TexturePackFolderModel::createParseTask(Resource& resource) { return new LocalTexturePackParseTask(m_next_resolution_ticket, static_cast(resource)); } + + +QVariant TexturePackFolderModel::data(const QModelIndex& index, int role) const +{ + if (!validateIndex(index)) + return {}; + + int row = index.row(); + int column = index.column(); + + switch (role) { + case Qt::DisplayRole: + switch (column) { + case NameColumn: + return m_resources[row]->name(); + case DateColumn: + return m_resources[row]->dateTimeChanged(); + default: + return {}; + } + case Qt::ToolTipRole: + if (column == NameColumn) { + if (at(row)->isSymLinkUnder(instDirPath())) { + return m_resources[row]->internal_id() + + tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original." + "\nCanonical Path: %1") + .arg(at(row)->fileinfo().canonicalFilePath());; + } + if (at(row)->isMoreThanOneHardLink()) { + return m_resources[row]->internal_id() + + tr("\nWarning: This resource is hard linked elsewhere. Editing it will also change the original."); + } + } + + return m_resources[row]->internal_id(); + case Qt::DecorationRole: { + if (column == NameColumn && (at(row)->isSymLinkUnder(instDirPath()) || at(row)->isMoreThanOneHardLink())) + return APPLICATION->getThemedIcon("status-yellow"); + if (column == ImageColumn) { + return at(row)->image({32, 32}, Qt::AspectRatioMode::KeepAspectRatioByExpanding); + } + return {}; + } + case Qt::CheckStateRole: + if (column == ActiveColumn) { + return m_resources[row]->enabled() ? Qt::Checked : Qt::Unchecked; + } + return {}; + default: + return {}; + } +} + +QVariant TexturePackFolderModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + switch (role) { + case Qt::DisplayRole: + switch (section) { + case ActiveColumn: + case NameColumn: + case DateColumn: + case ImageColumn: + return columnNames().at(section); + default: + return {}; + } + case Qt::ToolTipRole: { + switch (section) { + case ActiveColumn: + //: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc. + return tr("Is the resource enabled?"); + case NameColumn: + //: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc. + return tr("The name of the resource."); + case DateColumn: + //: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc. + return tr("The date and time this resource was last changed (or added)."); + default: + return {}; + } + } + default: + break; + } + + return {}; +} + +int TexturePackFolderModel::columnCount(const QModelIndex& parent) const +{ + return parent.isValid() ? 0 : NUM_COLUMNS; +} + diff --git a/launcher/minecraft/mod/TexturePackFolderModel.h b/launcher/minecraft/mod/TexturePackFolderModel.h index 425a71e46..71a8bdd16 100644 --- a/launcher/minecraft/mod/TexturePackFolderModel.h +++ b/launcher/minecraft/mod/TexturePackFolderModel.h @@ -38,12 +38,35 @@ #include "ResourceFolderModel.h" +#include "TexturePack.h" + class TexturePackFolderModel : public ResourceFolderModel { Q_OBJECT public: + + enum Columns + { + ActiveColumn = 0, + ImageColumn, + NameColumn, + DateColumn, + NUM_COLUMNS + }; + + explicit TexturePackFolderModel(const QString &dir, std::shared_ptr instance); + + virtual QString id() const override { return "texturepacks"; } + + [[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + [[nodiscard]] QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + [[nodiscard]] int columnCount(const QModelIndex &parent) const override; + explicit TexturePackFolderModel(const QString &dir, BaseInstance* instance); [[nodiscard]] Task* createUpdateTask() override; [[nodiscard]] Task* createParseTask(Resource&) override; + + RESOURCE_HELPERS(TexturePack) }; diff --git a/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp b/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp new file mode 100644 index 000000000..f8ecdb33e --- /dev/null +++ b/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp @@ -0,0 +1,252 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * 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 . + */ + +#include "GetModDependenciesTask.h" + +#include +#include +#include +#include "Json.h" +#include "QObjectPtr.h" +#include "minecraft/mod/MetadataHandler.h" +#include "modplatform/ModIndex.h" +#include "modplatform/ResourceAPI.h" +#include "modplatform/flame/FlameAPI.h" +#include "modplatform/modrinth/ModrinthAPI.h" +#include "tasks/ConcurrentTask.h" +#include "tasks/SequentialTask.h" +#include "ui/pages/modplatform/ModModel.h" +#include "ui/pages/modplatform/flame/FlameResourceModels.h" +#include "ui/pages/modplatform/modrinth/ModrinthResourceModels.h" + +static Version mcVersion(BaseInstance* inst) +{ + return static_cast(inst)->getPackProfile()->getComponent("net.minecraft")->getVersion(); +} + +static ResourceAPI::ModLoaderTypes mcLoaders(BaseInstance* inst) +{ + return static_cast(inst)->getPackProfile()->getModLoaders().value(); +} + +GetModDependenciesTask::GetModDependenciesTask(QObject* parent, + BaseInstance* instance, + ModFolderModel* folder, + QList> selected) + : SequentialTask(parent, tr("Get dependencies")) + , m_selected(selected) + , m_flame_provider{ ModPlatform::ResourceProvider::FLAME, std::make_shared(*instance), + std::make_shared() } + , m_modrinth_provider{ ModPlatform::ResourceProvider::MODRINTH, std::make_shared(*instance), + std::make_shared() } + , m_version(mcVersion(instance)) + , m_loaderType(mcLoaders(instance)) +{ + for (auto mod : folder->allMods()) + if (auto meta = mod->metadata(); meta) + m_mods.append(meta); + prepare(); +}; + +void GetModDependenciesTask::prepare() +{ + for (auto sel : m_selected) { + for (auto dep : getDependenciesForVersion(sel->version, sel->pack->provider)) { + addTask(prepareDependencyTask(dep, sel->pack->provider, 20)); + } + } +} + +ModPlatform::Dependency GetModDependenciesTask::getOverride(const ModPlatform::Dependency& dep, + const ModPlatform::ResourceProvider providerName) +{ + if (auto isQuilt = m_loaderType & ResourceAPI::Quilt; isQuilt || m_loaderType & ResourceAPI::Fabric) { + auto overide = ModPlatform::getOverrideDeps(); + auto over = std::find_if(overide.cbegin(), overide.cend(), [dep, providerName, isQuilt](auto o) { + return o.provider == providerName && dep.addonId == (isQuilt ? o.fabric : o.quilt); + }); + if (over != overide.cend()) { + return { isQuilt ? over->quilt : over->fabric, dep.type }; + } + } + return dep; +} + +QList GetModDependenciesTask::getDependenciesForVersion(const ModPlatform::IndexedVersion& version, + const ModPlatform::ResourceProvider providerName) +{ + QList c_dependencies; + for (auto ver_dep : version.dependencies) { + if (ver_dep.type != ModPlatform::DependencyType::REQUIRED) + continue; + + auto isOnlyVersion = providerName == ModPlatform::ResourceProvider::MODRINTH && ver_dep.addonId.toString().isEmpty(); + if (auto dep = std::find_if(c_dependencies.begin(), c_dependencies.end(), + [&ver_dep, isOnlyVersion](const ModPlatform::Dependency& i) { + return isOnlyVersion ? i.version == ver_dep.version : i.addonId == ver_dep.addonId; + }); + dep != c_dependencies.end()) + continue; // check the current dependency list + + if (auto dep = std::find_if(m_selected.begin(), m_selected.end(), + [&ver_dep, providerName, isOnlyVersion](std::shared_ptr i) { + return i->pack->provider == providerName && (isOnlyVersion ? i->version.version == ver_dep.version + : i->pack->addonId == ver_dep.addonId); + }); + dep != m_selected.end()) + continue; // check the selected versions + + if (auto dep = std::find_if(m_mods.begin(), m_mods.end(), + [&ver_dep, providerName, isOnlyVersion](std::shared_ptr i) { + return i->provider == providerName && + (isOnlyVersion ? i->file_id == ver_dep.version : i->project_id == ver_dep.addonId); + }); + dep != m_mods.end()) + continue; // check the existing mods + + if (auto dep = std::find_if(m_pack_dependencies.begin(), m_pack_dependencies.end(), + [&ver_dep, providerName, isOnlyVersion](std::shared_ptr i) { + return i->pack->provider == providerName && (isOnlyVersion ? i->version.version == ver_dep.addonId + : i->pack->addonId == ver_dep.addonId); + }); + dep != m_pack_dependencies.end()) // check loaded dependencies + continue; + + c_dependencies.append(getOverride(ver_dep, providerName)); + } + return c_dependencies; +}; + +Task::Ptr GetModDependenciesTask::getProjectInfoTask(std::shared_ptr pDep) +{ + auto provider = pDep->pack->provider == m_flame_provider.name ? m_flame_provider : m_modrinth_provider; + auto responseInfo = std::make_shared(); + auto info = provider.api->getProject(pDep->pack->addonId.toString(), responseInfo); + QObject::connect(info.get(), &NetJob::succeeded, [responseInfo, provider, pDep] { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*responseInfo, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response for mod info at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qDebug() << *responseInfo; + return; + } + try { + auto obj = provider.name == ModPlatform::ResourceProvider::FLAME ? Json::requireObject(Json::requireObject(doc), "data") + : Json::requireObject(doc); + provider.mod->loadIndexedPack(*pDep->pack, obj); + } catch (const JSONValidationError& e) { + qDebug() << doc; + qWarning() << "Error while reading mod info: " << e.cause(); + } + }); + return info; +} + +Task::Ptr GetModDependenciesTask::prepareDependencyTask(const ModPlatform::Dependency& dep, + const ModPlatform::ResourceProvider providerName, + int level) +{ + auto pDep = std::make_shared(); + pDep->dependency = dep; + pDep->pack = std::make_shared(); + pDep->pack->addonId = dep.addonId; + pDep->pack->provider = providerName; + + m_pack_dependencies.append(pDep); + auto provider = providerName == m_flame_provider.name ? m_flame_provider : m_modrinth_provider; + + auto tasks = makeShared( + this, QString("DependencyInfo: %1").arg(dep.addonId.toString().isEmpty() ? dep.version : dep.addonId.toString())); + + if (!dep.addonId.toString().isEmpty()) { + tasks->addTask(getProjectInfoTask(pDep)); + } + + ResourceAPI::DependencySearchArgs args = { dep, m_version, m_loaderType }; + ResourceAPI::DependencySearchCallbacks callbacks; + + callbacks.on_succeed = [dep, provider, pDep, level, this](auto& doc, auto& pack) { + try { + QJsonArray arr; + if (dep.version.length() != 0 && doc.isObject()) { + arr.append(doc.object()); + } else { + arr = doc.isObject() ? Json::ensureArray(doc.object(), "data") : doc.array(); + } + pDep->version = provider.mod->loadDependencyVersions(dep, arr); + if (!pDep->version.addonId.isValid()) { + if (m_loaderType & ResourceAPI::Quilt) { // falback for quilt + auto overide = ModPlatform::getOverrideDeps(); + auto over = std::find_if(overide.cbegin(), overide.cend(), + [dep, provider](auto o) { return o.provider == provider.name && dep.addonId == o.quilt; }); + if (over != overide.cend()) { + removePack(dep.addonId); + addTask(prepareDependencyTask({ over->fabric, dep.type }, provider.name, level)); + return; + } + } + qWarning() << "Error while reading mod version empty "; + qDebug() << doc; + return; + } + pDep->version.is_currently_selected = true; + pDep->pack->versions = { pDep->version }; + pDep->pack->versionsLoaded = true; + + } catch (const JSONValidationError& e) { + qDebug() << doc; + qWarning() << "Error while reading mod version: " << e.cause(); + return; + } + if (level == 0) { + qWarning() << "Dependency cycle exeeded"; + return; + } + if (dep.addonId.toString().isEmpty() && !pDep->version.addonId.toString().isEmpty()) { + pDep->pack->addonId = pDep->version.addonId; + auto dep = getOverride({ pDep->version.addonId, pDep->dependency.type }, provider.name); + if (dep.addonId != pDep->version.addonId) { + removePack(pDep->version.addonId); + addTask(prepareDependencyTask(dep, provider.name, level)); + } else + addTask(getProjectInfoTask(pDep)); + } + for (auto dep : getDependenciesForVersion(pDep->version, provider.name)) { + addTask(prepareDependencyTask(dep, provider.name, level - 1)); + } + }; + + auto version = provider.api->getDependencyVersion(std::move(args), std::move(callbacks)); + tasks->addTask(version); + return tasks; +}; + +void GetModDependenciesTask::removePack(const QVariant addonId) +{ + auto pred = [addonId](const std::shared_ptr& v) { return v->pack->addonId == addonId; }; +#if QT_VERSION >= QT_VERSION_CHECK(6, 1, 0) + m_pack_dependencies.removeIf(pred); +#else + for (auto it = m_pack_dependencies.begin(); it != m_pack_dependencies.end();) + if (pred(*it)) + it = m_pack_dependencies.erase(it); + else + ++it; +#endif +} diff --git a/launcher/minecraft/mod/tasks/GetModDependenciesTask.h b/launcher/minecraft/mod/tasks/GetModDependenciesTask.h new file mode 100644 index 000000000..50eba6afc --- /dev/null +++ b/launcher/minecraft/mod/tasks/GetModDependenciesTask.h @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * 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 . + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "minecraft/mod/MetadataHandler.h" +#include "minecraft/mod/ModFolderModel.h" +#include "modplatform/ModIndex.h" +#include "modplatform/ResourceAPI.h" +#include "tasks/SequentialTask.h" +#include "tasks/Task.h" +#include "ui/pages/modplatform/ModModel.h" + +class GetModDependenciesTask : public SequentialTask { + Q_OBJECT + public: + using Ptr = shared_qobject_ptr; + + struct PackDependency { + ModPlatform::Dependency dependency; + ModPlatform::IndexedPack::Ptr pack; + ModPlatform::IndexedVersion version; + PackDependency() = default; + PackDependency(const ModPlatform::IndexedPack::Ptr p, const ModPlatform::IndexedVersion& v) + { + pack = p; + version = v; + } + }; + + struct Provider { + ModPlatform::ResourceProvider name; + std::shared_ptr mod; + std::shared_ptr api; + }; + + explicit GetModDependenciesTask(QObject* parent, + BaseInstance* instance, + ModFolderModel* folder, + QList> selected); + + auto getDependecies() const -> QList> { return m_pack_dependencies; } + + protected slots: + Task::Ptr prepareDependencyTask(const ModPlatform::Dependency&, const ModPlatform::ResourceProvider, int); + QList getDependenciesForVersion(const ModPlatform::IndexedVersion&, + const ModPlatform::ResourceProvider providerName); + void prepare(); + Task::Ptr getProjectInfoTask(std::shared_ptr pDep); + ModPlatform::Dependency getOverride(const ModPlatform::Dependency&, const ModPlatform::ResourceProvider providerName); + void removePack(const QVariant addonId); + + private: + QList> m_pack_dependencies; + QList> m_mods; + QList> m_selected; + Provider m_flame_provider; + Provider m_modrinth_provider; + + Version m_version; + ResourceAPI::ModLoaderTypes m_loaderType; +}; diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp index 5342d693b..264019f84 100644 --- a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp @@ -52,6 +52,10 @@ ModDetails ReadMCModInfo(QByteArray contents) authors = firstObj.value("authors").toArray(); } + if (firstObj.contains("logoFile")) { + details.icon_file = firstObj.value("logoFile").toString(); + } + for (auto author : authors) { details.authors.append(author.toString()); } @@ -166,6 +170,31 @@ ModDetails ReadMCModTOML(QByteArray contents) } details.homeurl = homeurl; + QString issueTrackerURL = ""; + if (auto issueTrackerURLDatum = tomlData["issueTrackerURL"].as_string()) { + issueTrackerURL = QString::fromStdString(issueTrackerURLDatum->get()); + } else if (auto issueTrackerURLDatum = (*modsTable)["issueTrackerURL"].as_string()) { + issueTrackerURL = QString::fromStdString(issueTrackerURLDatum->get()); + } + details.issue_tracker = issueTrackerURL; + + QString license = ""; + if (auto licenseDatum = tomlData["license"].as_string()) { + license = QString::fromStdString(licenseDatum->get()); + } else if (auto licenseDatum =(*modsTable)["license"].as_string()) { + license = QString::fromStdString(licenseDatum->get()); + } + if (!license.isEmpty()) + details.licenses.append(ModLicense(license)); + + QString logoFile = ""; + if (auto logoFileDatum = tomlData["logoFile"].as_string()) { + logoFile = QString::fromStdString(logoFileDatum->get()); + } else if (auto logoFileDatum =(*modsTable)["logoFile"].as_string()) { + logoFile = QString::fromStdString(logoFileDatum->get()); + } + details.icon_file = logoFile; + return details; } @@ -201,6 +230,57 @@ ModDetails ReadFabricModInfo(QByteArray contents) if (contact.contains("homepage")) { details.homeurl = contact.value("homepage").toString(); } + if (contact.contains("issues")) { + details.issue_tracker = contact.value("issues").toString(); + } + } + + if (object.contains("license")) { + auto license = object.value("license"); + if (license.isArray()) { + for (auto l : license.toArray()) { + if (l.isString()) { + details.licenses.append(ModLicense(l.toString())); + } else if (l.isObject()) { + auto obj = l.toObject(); + details.licenses.append(ModLicense(obj.value("name").toString(), obj.value("id").toString(), + obj.value("url").toString(), obj.value("description").toString())); + } + } + } else if (license.isString()) { + details.licenses.append(ModLicense(license.toString())); + } else if (license.isObject()) { + auto obj = license.toObject(); + details.licenses.append(ModLicense(obj.value("name").toString(), obj.value("id").toString(), obj.value("url").toString(), + obj.value("description").toString())); + } + } + + if (object.contains("icon")) { + auto icon = object.value("icon"); + if (icon.isObject()) { + auto obj = icon.toObject(); + // take the largest icon + int largest = 0; + for (auto key : obj.keys()) { + auto size = key.split('x').first().toInt(); + if (size > largest) { + largest = size; + } + } + if (largest > 0) { + auto key = QString::number(largest) + "x" + QString::number(largest); + details.icon_file = obj.value(key).toString(); + } else { // parsing the sizes failed + // take the first + for (auto i : obj) { + details.icon_file = i.toString(); + break; + } + } + } else if (icon.isString()) { + details.icon_file = icon.toString(); + } } } return details; @@ -238,6 +318,58 @@ ModDetails ReadQuiltModInfo(QByteArray contents) if (modContact.contains("homepage")) { details.homeurl = Json::requireString(modContact.value("homepage")); } + if (modContact.contains("issues")) { + details.issue_tracker = Json::requireString(modContact.value("issues")); + } + + if (modMetadata.contains("license")) { + auto license = modMetadata.value("license"); + if (license.isArray()) { + for (auto l : license.toArray()) { + if (l.isString()) { + details.licenses.append(ModLicense(l.toString())); + } else if (l.isObject()) { + auto obj = l.toObject(); + details.licenses.append(ModLicense(obj.value("name").toString(), obj.value("id").toString(), + obj.value("url").toString(), obj.value("description").toString())); + } + } + } else if (license.isString()) { + details.licenses.append(ModLicense(license.toString())); + } else if (license.isObject()) { + auto obj = license.toObject(); + details.licenses.append(ModLicense(obj.value("name").toString(), obj.value("id").toString(), obj.value("url").toString(), + obj.value("description").toString())); + } + } + + if (modMetadata.contains("icon")) { + auto icon = modMetadata.value("icon"); + if (icon.isObject()) { + auto obj = icon.toObject(); + // take the largest icon + int largest = 0; + for (auto key : obj.keys()) { + auto size = key.split('x').first().toInt(); + if (size > largest) { + largest = size; + } + } + if (largest > 0) { + auto key = QString::number(largest) + "x" + QString::number(largest); + details.icon_file = obj.value(key).toString(); + } else { // parsing the sizes failed + // take the first + for (auto i : obj) { + details.icon_file = i.toString(); + break; + } + } + } else if (icon.isString()) { + details.icon_file = icon.toString(); + } + } + } return details; } @@ -515,6 +647,85 @@ bool validate(QFileInfo file) return ModUtils::process(mod, ProcessingLevel::BasicInfoOnly) && mod.valid(); } +bool processIconPNG(const Mod& mod, QByteArray&& raw_data) +{ + auto img = QImage::fromData(raw_data); + if (!img.isNull()) { + mod.setIcon(img); + } else { + qWarning() << "Failed to parse mod logo:" << mod.iconPath() << "from" << mod.name(); + return false; + } + return true; +} + +bool loadIconFile(const Mod& mod) { + if (mod.iconPath().isEmpty()) { + qWarning() << "No Iconfile set, be sure to parse the mod first"; + return false; + } + + auto png_invalid = [&mod]() { + qWarning() << "Mod at" << mod.fileinfo().filePath() << "does not have a valid icon"; + return false; + }; + + switch (mod.type()) { + case ResourceType::FOLDER: + { + QFileInfo icon_info(FS::PathCombine(mod.fileinfo().filePath(), mod.iconPath())); + if (icon_info.exists() && icon_info.isFile()) { + QFile icon(icon_info.filePath()); + if (!icon.open(QIODevice::ReadOnly)) + return false; + auto data = icon.readAll(); + + bool icon_result = ModUtils::processIconPNG(mod, std::move(data)); + + icon.close(); + + if (!icon_result) { + return png_invalid(); // icon invalid + } + } + } + case ResourceType::ZIPFILE: + { + QuaZip zip(mod.fileinfo().filePath()); + if (!zip.open(QuaZip::mdUnzip)) + return false; + + QuaZipFile file(&zip); + + if (zip.setCurrentFile(mod.iconPath())) { + if (!file.open(QIODevice::ReadOnly)) { + qCritical() << "Failed to open file in zip."; + zip.close(); + return png_invalid(); + } + + auto data = file.readAll(); + + bool icon_result = ModUtils::processIconPNG(mod, std::move(data)); + + file.close(); + if (!icon_result) { + return png_invalid(); // icon png invalid + } + } else { + return png_invalid(); // could not set icon as current file. + } + } + case ResourceType::LITEMOD: + { + return false; // can lightmods even have icons? + } + default: + qWarning() << "Invalid type for mod, can not load icon."; + return false; + } +} + } // namespace ModUtils LocalModParseTask::LocalModParseTask(int token, ResourceType type, const QFileInfo& modFile) diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.h b/launcher/minecraft/mod/tasks/LocalModParseTask.h index 38dae1357..a03217093 100644 --- a/launcher/minecraft/mod/tasks/LocalModParseTask.h +++ b/launcher/minecraft/mod/tasks/LocalModParseTask.h @@ -25,6 +25,9 @@ bool processLitemod(Mod& mod, ProcessingLevel level = ProcessingLevel::Full); /** Checks whether a file is valid as a mod or not. */ bool validate(QFileInfo file); + +bool processIconPNG(const Mod& mod, QByteArray&& raw_data); +bool loadIconFile(const Mod& mod); } // namespace ModUtils class LocalModParseTask : public Task { diff --git a/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp b/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp index cc4e252c1..4352fad91 100644 --- a/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp @@ -1,25 +1,24 @@ // SPDX-License-Identifier: GPL-3.0-only /* -* PolyMC - Minecraft Launcher -* Copyright (c) 2022 flowln -* Copyright (C) 2022 Sefa Eyeoglu -* -* 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 . -*/ + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * Copyright (C) 2022 Sefa Eyeoglu + * + * 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 . + */ #include "LocalModUpdateTask.h" -#include "Application.h" #include "FileSystem.h" #include "minecraft/mod/MetadataHandler.h" diff --git a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp index 4bf0b80d8..a67c56a8f 100644 --- a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp @@ -165,15 +165,16 @@ bool processZIP(ResourcePack& pack, ProcessingLevel level) bool pack_png_result = ResourcePackUtils::processPackPNG(pack, std::move(data)); file.close(); + zip.close(); if (!pack_png_result) { return png_invalid(); // pack.png invalid } } else { + zip.close(); return png_invalid(); // could not set pack.mcmeta as current file. } zip.close(); - return true; } @@ -193,7 +194,7 @@ bool processMCMeta(ResourcePack& pack, QByteArray&& raw_data) return true; } -bool processPackPNG(ResourcePack& pack, QByteArray&& raw_data) +bool processPackPNG(const ResourcePack& pack, QByteArray&& raw_data) { auto img = QImage::fromData(raw_data); if (!img.isNull()) { @@ -205,6 +206,68 @@ bool processPackPNG(ResourcePack& pack, QByteArray&& raw_data) return true; } +bool processPackPNG(const ResourcePack& pack) +{ + auto png_invalid = [&pack]() { + qWarning() << "Resource pack at" << pack.fileinfo().filePath() << "does not have a valid pack.png"; + return false; + }; + + switch (pack.type()) { + case ResourceType::FOLDER: + { + QFileInfo image_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.png")); + if (image_file_info.exists() && image_file_info.isFile()) { + QFile pack_png_file(image_file_info.filePath()); + if (!pack_png_file.open(QIODevice::ReadOnly)) + return png_invalid(); // can't open pack.png file + + auto data = pack_png_file.readAll(); + + bool pack_png_result = ResourcePackUtils::processPackPNG(pack, std::move(data)); + + pack_png_file.close(); + if (!pack_png_result) { + return png_invalid(); // pack.png invalid + } + } else { + return png_invalid(); // pack.png does not exists or is not a valid file. + } + } + case ResourceType::ZIPFILE: + { + Q_ASSERT(pack.type() == ResourceType::ZIPFILE); + + QuaZip zip(pack.fileinfo().filePath()); + if (!zip.open(QuaZip::mdUnzip)) + return false; // can't open zip file + + QuaZipFile file(&zip); + if (zip.setCurrentFile("pack.png")) { + if (!file.open(QIODevice::ReadOnly)) { + qCritical() << "Failed to open file in zip."; + zip.close(); + return png_invalid(); + } + + auto data = file.readAll(); + + bool pack_png_result = ResourcePackUtils::processPackPNG(pack, std::move(data)); + + file.close(); + if (!pack_png_result) { + return png_invalid(); // pack.png invalid + } + } else { + return png_invalid(); // could not set pack.mcmeta as current file. + } + } + default: + qWarning() << "Invalid type for resource pack parse task!"; + return false; + } +} + bool validate(QFileInfo file) { ResourcePack rp{ file }; diff --git a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h index d0c24c2b1..58d90b3b9 100644 --- a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h +++ b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h @@ -35,7 +35,10 @@ bool processZIP(ResourcePack& pack, ProcessingLevel level = ProcessingLevel::Ful bool processFolder(ResourcePack& pack, ProcessingLevel level = ProcessingLevel::Full); bool processMCMeta(ResourcePack& pack, QByteArray&& raw_data); -bool processPackPNG(ResourcePack& pack, QByteArray&& raw_data); +bool processPackPNG(const ResourcePack& pack, QByteArray&& raw_data); + +/// processes ONLY the pack.png (rest of the pack may be invalid) +bool processPackPNG(const ResourcePack& pack); /** Checks whether a file is valid as a resource pack or not. */ bool validate(QFileInfo file); diff --git a/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp index 38f1d7c1f..a72e81150 100644 --- a/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp @@ -131,6 +131,7 @@ bool processZIP(TexturePack& pack, ProcessingLevel level) bool packPNG_result = TexturePackUtils::processPackPNG(pack, std::move(data)); file.close(); + zip.close(); if (!packPNG_result) { return false; } @@ -147,7 +148,7 @@ bool processPackTXT(TexturePack& pack, QByteArray&& raw_data) return true; } -bool processPackPNG(TexturePack& pack, QByteArray&& raw_data) +bool processPackPNG(const TexturePack& pack, QByteArray&& raw_data) { auto img = QImage::fromData(raw_data); if (!img.isNull()) { @@ -159,6 +160,70 @@ bool processPackPNG(TexturePack& pack, QByteArray&& raw_data) return true; } +bool processPackPNG(const TexturePack& pack) +{ + auto png_invalid = [&pack]() { + qWarning() << "Texture pack at" << pack.fileinfo().filePath() << "does not have a valid pack.png"; + return false; + }; + + switch (pack.type()) { + case ResourceType::FOLDER: + { + QFileInfo image_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.png")); + if (image_file_info.exists() && image_file_info.isFile()) { + QFile pack_png_file(image_file_info.filePath()); + if (!pack_png_file.open(QIODevice::ReadOnly)) + return png_invalid(); // can't open pack.png file + + auto data = pack_png_file.readAll(); + + bool pack_png_result = TexturePackUtils::processPackPNG(pack, std::move(data)); + + pack_png_file.close(); + if (!pack_png_result) { + return png_invalid(); // pack.png invalid + } + } else { + return png_invalid(); // pack.png does not exists or is not a valid file. + } + } + case ResourceType::ZIPFILE: + { + Q_ASSERT(pack.type() == ResourceType::ZIPFILE); + + QuaZip zip(pack.fileinfo().filePath()); + if (!zip.open(QuaZip::mdUnzip)) + return false; // can't open zip file + + QuaZipFile file(&zip); + if (zip.setCurrentFile("pack.png")) { + if (!file.open(QIODevice::ReadOnly)) { + qCritical() << "Failed to open file in zip."; + zip.close(); + return png_invalid(); + } + + auto data = file.readAll(); + + bool pack_png_result = TexturePackUtils::processPackPNG(pack, std::move(data)); + + file.close(); + if (!pack_png_result) { + zip.close(); + return png_invalid(); // pack.png invalid + } + } else { + zip.close(); + return png_invalid(); // could not set pack.mcmeta as current file. + } + } + default: + qWarning() << "Invalid type for resource pack parse task!"; + return false; + } +} + bool validate(QFileInfo file) { TexturePack rp{ file }; diff --git a/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.h b/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.h index 1589f8cbd..6b91565ad 100644 --- a/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.h +++ b/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.h @@ -36,7 +36,10 @@ bool processZIP(TexturePack& pack, ProcessingLevel level = ProcessingLevel::Full bool processFolder(TexturePack& pack, ProcessingLevel level = ProcessingLevel::Full); bool processPackTXT(TexturePack& pack, QByteArray&& raw_data); -bool processPackPNG(TexturePack& pack, QByteArray&& raw_data); +bool processPackPNG(const TexturePack& pack, QByteArray&& raw_data); + +/// processes ONLY the pack.png (rest of the pack may be invalid) +bool processPackPNG(const TexturePack& pack); /** Checks whether a file is valid as a texture pack or not. */ bool validate(QFileInfo file); diff --git a/launcher/modplatform/EnsureMetadataTask.cpp b/launcher/modplatform/EnsureMetadataTask.cpp index 34d969f02..93b5ce76c 100644 --- a/launcher/modplatform/EnsureMetadataTask.cpp +++ b/launcher/modplatform/EnsureMetadataTask.cpp @@ -10,6 +10,7 @@ #include "modplatform/flame/FlameAPI.h" #include "modplatform/flame/FlameModIndex.h" +#include "modplatform/helpers/HashUtils.h" #include "modplatform/modrinth/ModrinthAPI.h" #include "modplatform/modrinth/ModrinthPackIndex.h" @@ -24,8 +25,8 @@ EnsureMetadataTask::EnsureMetadataTask(Mod* mod, QDir dir, ModPlatform::Resource auto hash_task = createNewHash(mod); if (!hash_task) return; - connect(hash_task.get(), &Task::succeeded, [this, hash_task, mod] { m_mods.insert(hash_task->getResult(), mod); }); - connect(hash_task.get(), &Task::failed, [this, hash_task, mod] { emitFail(mod, "", RemoveFromList::No); }); + connect(hash_task.get(), &Hashing::Hasher::resultsReady, [this, mod](QString hash) { m_mods.insert(hash, mod); }); + connect(hash_task.get(), &Task::failed, [this, mod] { emitFail(mod, "", RemoveFromList::No); }); hash_task->start(); } @@ -37,8 +38,8 @@ EnsureMetadataTask::EnsureMetadataTask(QList& mods, QDir dir, ModPlatform: auto hash_task = createNewHash(mod); if (!hash_task) continue; - connect(hash_task.get(), &Task::succeeded, [this, hash_task, mod] { m_mods.insert(hash_task->getResult(), mod); }); - connect(hash_task.get(), &Task::failed, [this, hash_task, mod] { emitFail(mod, "", RemoveFromList::No); }); + connect(hash_task.get(), &Hashing::Hasher::resultsReady, [this, mod](QString hash) { m_mods.insert(hash, mod); }); + connect(hash_task.get(), &Task::failed, [this, mod] { emitFail(mod, "", RemoveFromList::No); }); m_hashing_task->addTask(hash_task); } } @@ -212,12 +213,12 @@ Task::Ptr EnsureMetadataTask::modrinthVersionsTask() { auto hash_type = ProviderCaps.hashType(ModPlatform::ResourceProvider::MODRINTH).first(); - auto* response = new QByteArray(); + auto response = std::make_shared(); auto ver_task = modrinth_api.currentVersions(m_mods.keys(), hash_type, response); // Prevents unfortunate timings when aborting the task if (!ver_task) - return Task::Ptr{nullptr}; + return Task::Ptr{ nullptr }; connect(ver_task.get(), &Task::succeeded, this, [this, response] { QJsonParseError parse_error{}; @@ -264,7 +265,7 @@ Task::Ptr EnsureMetadataTask::modrinthProjectsTask() for (auto const& data : m_temp_versions) addonIds.insert(data.addonId.toString(), data.hash); - auto response = new QByteArray(); + auto response = std::make_shared(); Task::Ptr proj_task; if (addonIds.isEmpty()) { @@ -277,7 +278,7 @@ Task::Ptr EnsureMetadataTask::modrinthProjectsTask() // Prevents unfortunate timings when aborting the task if (!proj_task) - return Task::Ptr{nullptr}; + return Task::Ptr{ nullptr }; connect(proj_task.get(), &Task::succeeded, this, [this, response, addonIds] { QJsonParseError parse_error{}; @@ -345,7 +346,7 @@ Task::Ptr EnsureMetadataTask::modrinthProjectsTask() // Flame Task::Ptr EnsureMetadataTask::flameVersionsTask() { - auto* response = new QByteArray(); + auto response = std::make_shared(); QList fingerprints; for (auto& murmur : m_mods.keys()) { @@ -413,7 +414,7 @@ Task::Ptr EnsureMetadataTask::flameProjectsTask() QHash addonIds; for (auto const& hash : m_mods.keys()) { if (m_temp_versions.contains(hash)) { - auto const& data = m_temp_versions.find(hash).value(); + auto data = m_temp_versions.find(hash).value(); auto id_str = data.addonId.toString(); if (!id_str.isEmpty()) @@ -421,7 +422,7 @@ Task::Ptr EnsureMetadataTask::flameProjectsTask() } } - auto response = new QByteArray(); + auto response = std::make_shared(); Task::Ptr proj_task; if (addonIds.isEmpty()) { @@ -434,7 +435,7 @@ Task::Ptr EnsureMetadataTask::flameProjectsTask() // Prevents unfortunate timings when aborting the task if (!proj_task) - return Task::Ptr{nullptr}; + return Task::Ptr{ nullptr }; connect(proj_task.get(), &Task::succeeded, this, [this, response, addonIds] { QJsonParseError parse_error{}; diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h index a4d431e30..b76700511 100644 --- a/launcher/modplatform/ModIndex.h +++ b/launcher/modplatform/ModIndex.h @@ -1,20 +1,21 @@ // SPDX-License-Identifier: GPL-3.0-only /* -* PolyMC - Minecraft Launcher -* Copyright (c) 2022 flowln -* -* 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 . -*/ + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * Copyright (c) 2023 Trial97 + * + * 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 . + */ #pragma once @@ -33,6 +34,8 @@ enum class ResourceProvider { MODRINTH, FLAME }; enum class ResourceType { MOD, RESOURCE_PACK, SHADER_PACK }; +enum class DependencyType { REQUIRED, OPTIONAL, INCOMPATIBLE, EMBEDDED, TOOL, INCLUDE, UNKNOWN }; + class ProviderCapabilities { public: auto name(ResourceProvider) -> const char*; @@ -52,6 +55,12 @@ struct DonationData { QString url; }; +struct Dependency { + QVariant addonId; + DependencyType type; + QString version; +}; + struct IndexedVersion { QVariant addonId; QVariant fileId; @@ -66,10 +75,10 @@ struct IndexedVersion { QString hash; bool is_preferred = true; QString changelog; + QList dependencies; // For internal use, not provided by APIs bool is_currently_selected = false; - QString custom_target_folder; }; struct ExtraPackData { @@ -116,12 +125,28 @@ struct IndexedPack { if (!versionsLoaded) return false; - return std::any_of(versions.constBegin(), versions.constEnd(), - [](auto const& v) { return v.is_currently_selected; }); + return std::any_of(versions.constBegin(), versions.constEnd(), [](auto const& v) { return v.is_currently_selected; }); } }; +struct OverrideDep { + QString quilt; + QString fabric; + QString slug; + ModPlatform::ResourceProvider provider; +}; + +inline auto getOverrideDeps() -> QList +{ + return { { "634179", "306612", "API", ModPlatform::ResourceProvider::FLAME }, + { "720410", "308769", "KotlinLibraries", ModPlatform::ResourceProvider::FLAME }, + + { "qvIfYCYJ", "P7dR8mSH", "API", ModPlatform::ResourceProvider::MODRINTH }, + { "lwVhp9o5", "Ha28R6CL", "KotlinLibraries", ModPlatform::ResourceProvider::MODRINTH } }; +}; + } // namespace ModPlatform Q_DECLARE_METATYPE(ModPlatform::IndexedPack) +Q_DECLARE_METATYPE(ModPlatform::IndexedPack::Ptr) Q_DECLARE_METATYPE(ModPlatform::ResourceProvider) diff --git a/launcher/modplatform/ResourceAPI.h b/launcher/modplatform/ResourceAPI.h index d4c9af8ad..a8f300b4e 100644 --- a/launcher/modplatform/ResourceAPI.h +++ b/launcher/modplatform/ResourceAPI.h @@ -111,6 +111,16 @@ class ResourceAPI { std::function on_succeed; }; + struct DependencySearchArgs { + ModPlatform::Dependency dependency; + Version mcVersion; + ModLoaderTypes loader; + }; + + struct DependencySearchCallbacks { + std::function on_succeed; + }; + public: /** Gets a list of available sorting methods for this API. */ [[nodiscard]] virtual auto getSortingMethods() const -> QList = 0; @@ -121,14 +131,12 @@ class ResourceAPI { qWarning() << "TODO: ResourceAPI::searchProjects"; return nullptr; } - /** getProject(QString addonId, QByteArray* responce)*/ - [[nodiscard]] virtual Task::Ptr getProject(QString, QByteArray*) const + [[nodiscard]] virtual Task::Ptr getProject(QString addonId, std::shared_ptr response) const { qWarning() << "TODO: ResourceAPI::getProject"; return nullptr; } - /** getProjects(QStringList addonIds, QByteArray* responce) */ - [[nodiscard]] virtual Task::Ptr getProjects(QStringList, QByteArray*) const + [[nodiscard]] virtual Task::Ptr getProjects(QStringList addonIds, std::shared_ptr response) const { qWarning() << "TODO: ResourceAPI::getProjects"; return nullptr; @@ -145,6 +153,12 @@ class ResourceAPI { return nullptr; } + [[nodiscard]] virtual Task::Ptr getDependencyVersion(DependencySearchArgs&&, DependencySearchCallbacks&&) const + { + qWarning() << "TODO"; + return nullptr; + } + static auto getModLoaderString(ModLoaderType type) -> const QString { switch (type) { diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp index 96cea7b7d..22ea02da2 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp @@ -82,9 +82,9 @@ void PackInstallTask::executeTask() { qDebug() << "PackInstallTask::executeTask: " << QThread::currentThreadId(); NetJob::Ptr netJob{ new NetJob("ATLauncher::VersionFetch", APPLICATION->network()) }; - auto searchUrl = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "packs/%1/versions/%2/Configs.json") - .arg(m_pack_safe_name).arg(m_version_name); - netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); + auto searchUrl = + QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "packs/%1/versions/%2/Configs.json").arg(m_pack_safe_name).arg(m_version_name); + netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), response)); QObject::connect(netJob.get(), &NetJob::succeeded, this, &PackInstallTask::onDownloadSucceeded); QObject::connect(netJob.get(), &NetJob::failed, this, &PackInstallTask::onDownloadFailed); @@ -99,11 +99,12 @@ void PackInstallTask::onDownloadSucceeded() qDebug() << "PackInstallTask::onDownloadSucceeded: " << QThread::currentThreadId(); jobPtr.reset(); - QJsonParseError parse_error {}; - QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); - if(parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from ATLauncher at " << parse_error.offset << " reason: " << parse_error.errorString(); - qWarning() << response; + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from ATLauncher at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response.get(); return; } auto obj = doc.object(); @@ -352,7 +353,7 @@ QString PackInstallTask::getVersionForLoader(QString uid) if(m_version.loader.recommended || m_version.loader.latest) { for (int i = 0; i < vlist->versions().size(); i++) { auto version = vlist->versions().at(i); - auto reqs = version->requires(); + auto reqs = version->requiredSet(); // filter by minecraft version, if the loader depends on a certain version. // not all mod loaders depend on a given Minecraft version, so we won't do this diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.h b/launcher/modplatform/atlauncher/ATLPackInstallTask.h index 90e25ae2e..b82f523fe 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.h +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.h @@ -40,12 +40,13 @@ #include "ATLPackManifest.h" #include "InstanceTask.h" -#include "net/NetJob.h" -#include "settings/INISettingsObject.h" +#include "meta/Version.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" -#include "meta/Version.h" +#include "net/NetJob.h" +#include "settings/INISettingsObject.h" +#include #include namespace ATLauncher { @@ -57,8 +58,7 @@ enum class InstallMode { }; class UserInteractionSupport { - -public: + public: /** * Requests a user interaction to select which optional mods should be installed. */ @@ -74,23 +74,27 @@ public: * Requests a user interaction to display a message. */ virtual void displayMessage(QString message) = 0; + + virtual ~UserInteractionSupport() = default; }; -class PackInstallTask : public InstanceTask -{ -Q_OBJECT +class PackInstallTask : public InstanceTask { + Q_OBJECT -public: - explicit PackInstallTask(UserInteractionSupport *support, QString packName, QString version, InstallMode installMode = InstallMode::Install); - virtual ~PackInstallTask(){} + public: + explicit PackInstallTask(UserInteractionSupport* support, + QString packName, + QString version, + InstallMode installMode = InstallMode::Install); + virtual ~PackInstallTask() { delete m_support; } bool canAbort() const override { return true; } bool abort() override; -protected: + protected: virtual void executeTask() override; -private slots: + private slots: void onDownloadSucceeded(); void onDownloadFailed(QString reason); void onDownloadAborted(); @@ -98,7 +102,7 @@ private slots: void onModsDownloaded(); void onModsExtracted(); -private: + private: QString getDirForModType(ModType type, QString raw); QString getVersionForLoader(QString uid); QString detectLibrary(VersionLibrary library); @@ -110,20 +114,18 @@ private: void installConfigs(); void extractConfigs(); void downloadMods(); - bool extractMods( - const QMap &toExtract, - const QMap &toDecomp, - const QMap &toCopy - ); + bool extractMods(const QMap& toExtract, + const QMap& toDecomp, + const QMap& toCopy); void install(); -private: - UserInteractionSupport *m_support; + private: + UserInteractionSupport* m_support; bool abortable = false; NetJob::Ptr jobPtr; - QByteArray response; + std::shared_ptr response = std::make_shared(); InstallMode m_install_mode; QString m_pack_name; @@ -145,7 +147,6 @@ private: QFuture m_modExtractFuture; QFutureWatcher m_modExtractFutureWatcher; - }; -} +} // namespace ATLauncher diff --git a/launcher/modplatform/flame/FileResolvingTask.cpp b/launcher/modplatform/flame/FileResolvingTask.cpp index 83db642e7..ce7a60551 100644 --- a/launcher/modplatform/flame/FileResolvingTask.cpp +++ b/launcher/modplatform/flame/FileResolvingTask.cpp @@ -25,15 +25,16 @@ void Flame::FileResolvingTask::executeTask() setProgress(0, 3); m_dljob.reset(new NetJob("Mod id resolver", m_network)); result.reset(new QByteArray()); - //build json data to send + // build json data to send QJsonObject object; - object["fileIds"] = QJsonArray::fromVariantList(std::accumulate(m_toProcess.files.begin(), m_toProcess.files.end(), QVariantList(), [](QVariantList& l, const File& s) { - l.push_back(s.fileId); - return l; - })); + object["fileIds"] = QJsonArray::fromVariantList( + std::accumulate(m_toProcess.files.begin(), m_toProcess.files.end(), QVariantList(), [](QVariantList& l, const File& s) { + l.push_back(s.fileId); + return l; + })); QByteArray data = Json::toText(object); - auto dl = Net::Upload::makeByteArray(QUrl("https://api.curseforge.com/v1/mods/files"), result.get(), data); + auto dl = Net::Upload::makeByteArray(QUrl("https://api.curseforge.com/v1/mods/files"), result, data); m_dljob->addNetAction(dl); auto step_progress = std::make_shared(); @@ -87,17 +88,15 @@ void Flame::FileResolvingTask::netJobFinished() auto fileid = Json::requireInteger(Json::requireObject(file)["id"]); auto& out = m_toProcess.files[fileid]; try { - out.parseFromObject(Json::requireObject(file)); + out.parseFromObject(Json::requireObject(file)); } catch (const JSONValidationError& e) { qDebug() << "Blocked mod on curseforge" << out.fileName; auto hash = out.hash; - if(!hash.isEmpty()) { + if (!hash.isEmpty()) { auto url = QString("https://api.modrinth.com/v2/version_file/%1?algorithm=sha1").arg(hash); auto output = std::make_shared(); - auto dl = Net::Download::makeByteArray(QUrl(url), output.get()); - QObject::connect(dl.get(), &Net::Download::succeeded, [&out]() { - out.resolved = true; - }); + auto dl = Net::Download::makeByteArray(QUrl(url), output); + QObject::connect(dl.get(), &Net::Download::succeeded, [&out]() { out.resolved = true; }); m_checkJob->addNetAction(dl); blockedProjects.insert(&out, output); @@ -169,7 +168,7 @@ void Flame::FileResolvingTask::modrinthCheckFinished() { auto projectId = mod->projectId; auto output = std::make_shared(); auto url = QString("https://api.curseforge.com/v1/mods/%1").arg(projectId); - auto dl = Net::Download::makeByteArray(url, output.get()); + auto dl = Net::Download::makeByteArray(url, output); qDebug() << "Fetching url slug for file:" << mod->fileName; QObject::connect(dl.get(), &Net::Download::succeeded, [block, index, output]() { auto mod = block->at(index); // use the shared_ptr so it is captured and only freed when we are done diff --git a/launcher/modplatform/flame/FlameAPI.cpp b/launcher/modplatform/flame/FlameAPI.cpp index 92590a084..5b0b1d8b9 100644 --- a/launcher/modplatform/flame/FlameAPI.cpp +++ b/launcher/modplatform/flame/FlameAPI.cpp @@ -11,7 +11,7 @@ #include "net/NetJob.h" #include "net/Upload.h" -Task::Ptr FlameAPI::matchFingerprints(const QList& fingerprints, QByteArray* response) +Task::Ptr FlameAPI::matchFingerprints(const QList& fingerprints, std::shared_ptr response) { auto netJob = makeShared(QString("Flame::MatchFingerprints"), APPLICATION->network()); @@ -28,8 +28,6 @@ Task::Ptr FlameAPI::matchFingerprints(const QList& fingerprints, QByteArra netJob->addNetAction(Net::Upload::makeByteArray(QString("https://api.curseforge.com/v1/fingerprints"), response, body_raw)); - QObject::connect(netJob.get(), &NetJob::finished, [response] { delete response; }); - return netJob; } @@ -43,7 +41,7 @@ auto FlameAPI::getModFileChangelog(int modId, int fileId) -> QString netJob->addNetAction(Net::Download::makeByteArray( QString("https://api.curseforge.com/v1/mods/%1/files/%2/changelog") .arg(QString::fromStdString(std::to_string(modId)), QString::fromStdString(std::to_string(fileId))), - response.get())); + response)); QObject::connect(netJob.get(), &NetJob::succeeded, [&netJob, response, &changelog] { QJsonParseError parse_error{}; @@ -75,8 +73,8 @@ auto FlameAPI::getModDescription(int modId) -> QString auto netJob = makeShared(QString("Flame::ModDescription"), APPLICATION->network()); auto response = std::make_shared(); - netJob->addNetAction(Net::Download::makeByteArray( - QString("https://api.curseforge.com/v1/mods/%1/description").arg(QString::number(modId)), response.get())); + netJob->addNetAction( + Net::Download::makeByteArray(QString("https://api.curseforge.com/v1/mods/%1/description").arg(QString::number(modId)), response)); QObject::connect(netJob.get(), &NetJob::succeeded, [&netJob, response, &description] { QJsonParseError parse_error{}; @@ -115,7 +113,7 @@ auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::Indexe auto response = std::make_shared(); ModPlatform::IndexedVersion ver; - netJob->addNetAction(Net::Download::makeByteArray(versions_url, response.get())); + netJob->addNetAction(Net::Download::makeByteArray(versions_url, response)); QObject::connect(netJob.get(), &NetJob::succeeded, [response, args, &ver] { QJsonParseError parse_error{}; @@ -137,7 +135,7 @@ auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::Indexe for (auto file : arr) { auto file_obj = Json::requireObject(file); auto file_tmp = FlameMod::loadIndexedPackVersion(file_obj); - if(file_tmp.date > ver_tmp.date) { + if (file_tmp.date > ver_tmp.date) { ver_tmp = file_tmp; latest_file_obj = file_obj; } @@ -160,7 +158,7 @@ auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::Indexe return ver; } -Task::Ptr FlameAPI::getProjects(QStringList addonIds, QByteArray* response) const +Task::Ptr FlameAPI::getProjects(QStringList addonIds, std::shared_ptr response) const { auto netJob = makeShared(QString("Flame::GetProjects"), APPLICATION->network()); @@ -177,13 +175,12 @@ Task::Ptr FlameAPI::getProjects(QStringList addonIds, QByteArray* response) cons netJob->addNetAction(Net::Upload::makeByteArray(QString("https://api.curseforge.com/v1/mods"), response, body_raw)); - QObject::connect(netJob.get(), &NetJob::finished, [response] { delete response; }); QObject::connect(netJob.get(), &NetJob::failed, [body_raw] { qDebug() << body_raw; }); return netJob; } -Task::Ptr FlameAPI::getFiles(const QStringList& fileIds, QByteArray* response) const +Task::Ptr FlameAPI::getFiles(const QStringList& fileIds, std::shared_ptr response) const { auto netJob = makeShared(QString("Flame::GetFiles"), APPLICATION->network()); @@ -200,7 +197,6 @@ Task::Ptr FlameAPI::getFiles(const QStringList& fileIds, QByteArray* response) c netJob->addNetAction(Net::Upload::makeByteArray(QString("https://api.curseforge.com/v1/mods/files"), response, body_raw)); - QObject::connect(netJob.get(), &NetJob::finished, [response] { delete response; }); QObject::connect(netJob.get(), &NetJob::failed, [body_raw] { qDebug() << body_raw; }); return netJob; diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index 5811d7175..0a6dc78f8 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -4,7 +4,10 @@ #pragma once +#include +#include #include "modplatform/ModIndex.h" +#include "modplatform/ResourceAPI.h" #include "modplatform/helpers/NetworkResourceAPI.h" class FlameAPI : public NetworkResourceAPI { @@ -14,9 +17,9 @@ class FlameAPI : public NetworkResourceAPI { auto getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::IndexedVersion; - Task::Ptr getProjects(QStringList addonIds, QByteArray* response) const override; - Task::Ptr matchFingerprints(const QList& fingerprints, QByteArray* response); - Task::Ptr getFiles(const QStringList& fileIds, QByteArray* response) const; + Task::Ptr getProjects(QStringList addonIds, std::shared_ptr response) const override; + Task::Ptr matchFingerprints(const QList& fingerprints, std::shared_ptr response); + Task::Ptr getFiles(const QStringList& fileIds, std::shared_ptr response) const; [[nodiscard]] auto getSortingMethods() const -> QList override; @@ -41,14 +44,15 @@ class FlameAPI : public NetworkResourceAPI { return 4; // TODO: remove this once Quilt drops official Fabric support if (loaders & Quilt) // NOTE: Most if not all Fabric mods should work *currently* - return 4; // Quilt would probably be 5 + return 4; // Quilt would probably be 5 return 0; } private: [[nodiscard]] std::optional getSearchURL(SearchArgs const& args) const override { - auto gameVersionStr = args.versions.has_value() ? QString("gameVersion=%1").arg(args.versions.value().front().toString()) : QString(); + auto gameVersionStr = + args.versions.has_value() ? QString("gameVersion=%1").arg(args.versions.value().front().toString()) : QString(); QStringList get_arguments; get_arguments.append(QString("classId=%1").arg(getClassId(args.type))); @@ -73,14 +77,48 @@ class FlameAPI : public NetworkResourceAPI { [[nodiscard]] std::optional getVersionsURL(VersionSearchArgs const& args) const override { - QString url{QString("https://api.curseforge.com/v1/mods/%1/files?pageSize=10000&").arg(args.pack.addonId.toString())}; + auto addonId = args.pack.addonId.toString(); + QString url{ QString("https://api.curseforge.com/v1/mods/%1/files?pageSize=10000&").arg(addonId) }; QStringList get_parameters; if (args.mcVersions.has_value()) get_parameters.append(QString("gameVersion=%1").arg(args.mcVersions.value().front().toString())); - if (args.loaders.has_value()) - get_parameters.append(QString("modLoaderType=%1").arg(getMappedModLoader(args.loaders.value()))); + + if (args.loaders.has_value()) { + int mappedModLoader = getMappedModLoader(args.loaders.value()); + + if (args.loaders.value() & Quilt) { + auto overide = ModPlatform::getOverrideDeps(); + auto over = std::find_if(overide.cbegin(), overide.cend(), [addonId](auto dep) { + return dep.provider == ModPlatform::ResourceProvider::FLAME && addonId == dep.quilt; + }); + if (over != overide.cend()) { + mappedModLoader = 5; + } + } + + get_parameters.append(QString("modLoaderType=%1").arg(mappedModLoader)); + } return url + get_parameters.join('&'); }; + + [[nodiscard]] std::optional getDependencyURL(DependencySearchArgs const& args) const override + { + auto mappedModLoader = getMappedModLoader(args.loader); + auto addonId = args.dependency.addonId.toString(); + if (args.loader & Quilt) { + auto overide = ModPlatform::getOverrideDeps(); + auto over = std::find_if(overide.cbegin(), overide.cend(), [addonId](auto dep) { + return dep.provider == ModPlatform::ResourceProvider::FLAME && addonId == dep.quilt; + }); + if (over != overide.cend()) { + mappedModLoader = 5; + } + } + return QString("https://api.curseforge.com/v1/mods/%1/files?pageSize=10000&gameVersion=%2&modLoaderType=%3") + .arg(addonId) + .arg(args.mcVersion.toString()) + .arg(mappedModLoader); + }; }; diff --git a/launcher/modplatform/flame/FlameCheckUpdate.cpp b/launcher/modplatform/flame/FlameCheckUpdate.cpp index 06a895027..a2628e34c 100644 --- a/launcher/modplatform/flame/FlameCheckUpdate.cpp +++ b/launcher/modplatform/flame/FlameCheckUpdate.cpp @@ -3,6 +3,7 @@ #include "FlameModIndex.h" #include +#include #include "FileSystem.h" #include "Json.h" @@ -30,7 +31,7 @@ ModPlatform::IndexedPack getProjectInfo(ModPlatform::IndexedVersion& ver_info) auto get_project_job = new NetJob("Flame::GetProjectJob", APPLICATION->network()); - auto response = new QByteArray(); + auto response = std::make_shared(); auto url = QString("https://api.curseforge.com/v1/mods/%1").arg(ver_info.addonId.toString()); auto dl = Net::Download::makeByteArray(url, response); get_project_job->addNetAction(dl); @@ -74,7 +75,7 @@ ModPlatform::IndexedVersion getFileInfo(int addonId, int fileId) auto get_file_info_job = new NetJob("Flame::GetFileInfoJob", APPLICATION->network()); - auto response = new QByteArray(); + auto response = std::make_shared(); auto url = QString("https://api.curseforge.com/v1/mods/%1/files/%2").arg(QString::number(addonId), QString::number(fileId)); auto dl = Net::Download::makeByteArray(url, response); get_file_info_job->addNetAction(dl); @@ -129,8 +130,7 @@ void FlameCheckUpdate::executeTask() setStatus(tr("Getting API response from CurseForge for '%1'...").arg(mod->name())); setProgress(i++, m_mods.size()); - ModPlatform::IndexedPack pack{ mod->metadata()->project_id.toString() }; - auto latest_ver = api.getLatestVersion({ pack, m_game_versions, m_loaders }); + auto latest_ver = api.getLatestVersion({ { mod->metadata()->project_id.toString() }, m_game_versions, m_loaders }); // Check if we were aborted while getting the latest version if (m_was_aborted) { @@ -156,15 +156,15 @@ void FlameCheckUpdate::executeTask() if (!latest_ver.hash.isEmpty() && (mod->metadata()->hash != latest_ver.hash || mod->status() == ModStatus::NotInstalled)) { // Fake pack with the necessary info to pass to the download task :) - ModPlatform::IndexedPack pack; - pack.name = mod->name(); - pack.slug = mod->metadata()->slug; - pack.addonId = mod->metadata()->project_id; - pack.websiteUrl = mod->homeurl(); + auto pack = std::make_shared(); + 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(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 dae93d1c6..f003ada9d 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--; } } @@ -179,7 +182,7 @@ bool FlameCreationTask::updateInstance() fileIds.append(QString::number(file.fileId)); } - auto* raw_response = new QByteArray; + auto raw_response = std::make_shared(); auto job = api.getFiles(fileIds, raw_response); QEventLoop loop; diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp index 7498e8302..227ce4898 100644 --- a/launcher/modplatform/flame/FlameModIndex.cpp +++ b/launcher/modplatform/flame/FlameModIndex.cpp @@ -39,15 +39,15 @@ void FlameMod::loadURLs(ModPlatform::IndexedPack& pack, QJsonObject& obj) auto links_obj = Json::ensureObject(obj, "links"); pack.extraData.issuesUrl = Json::ensureString(links_obj, "issuesUrl"); - if(pack.extraData.issuesUrl.endsWith('/')) + if (pack.extraData.issuesUrl.endsWith('/')) pack.extraData.issuesUrl.chop(1); pack.extraData.sourceUrl = Json::ensureString(links_obj, "sourceUrl"); - if(pack.extraData.sourceUrl.endsWith('/')) + if (pack.extraData.sourceUrl.endsWith('/')) pack.extraData.sourceUrl.chop(1); pack.extraData.wikiUrl = Json::ensureString(links_obj, "wikiUrl"); - if(pack.extraData.wikiUrl.endsWith('/')) + if (pack.extraData.wikiUrl.endsWith('/')) pack.extraData.wikiUrl.chop(1); if (!pack.extraData.body.isEmpty()) @@ -56,7 +56,7 @@ void FlameMod::loadURLs(ModPlatform::IndexedPack& pack, QJsonObject& obj) void FlameMod::loadBody(ModPlatform::IndexedPack& pack, QJsonObject& obj) { - pack.extraData.body = api.getModDescription(pack.addonId.toInt()); + pack.extraData.body = api.getModDescription(pack.addonId.toInt()); if (!pack.extraData.issuesUrl.isEmpty() || !pack.extraData.sourceUrl.isEmpty() || !pack.extraData.wikiUrl.isEmpty()) pack.extraDataLoaded = true; @@ -64,12 +64,12 @@ void FlameMod::loadBody(ModPlatform::IndexedPack& pack, QJsonObject& obj) static QString enumToString(int hash_algorithm) { - switch(hash_algorithm){ - default: - case 1: - return "sha1"; - case 2: - return "md5"; + switch (hash_algorithm) { + default: + case 1: + return "sha1"; + case 2: + return "md5"; } } @@ -84,12 +84,12 @@ void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, for (auto versionIter : arr) { auto obj = versionIter.toObject(); - + auto file = loadIndexedPackVersion(obj); - if(!file.addonId.isValid()) + if (!file.addonId.isValid()) file.addonId = pack.addonId; - if(file.fileId.isValid()) // Heuristic to check if the returned value is valid + if (file.fileId.isValid()) // Heuristic to check if the returned value is valid unsortedVersions.append(file); } @@ -136,8 +136,61 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) -> } } - if(load_changelog) + auto dependencies = Json::ensureArray(obj, "dependencies"); + for (auto d : dependencies) { + auto dep = Json::ensureObject(d); + ModPlatform::Dependency dependency; + dependency.addonId = Json::requireInteger(dep, "modId"); + switch (Json::requireInteger(dep, "relationType")) { + case 1: // EmbeddedLibrary + dependency.type = ModPlatform::DependencyType::EMBEDDED; + break; + case 2: // OptionalDependency + dependency.type = ModPlatform::DependencyType::OPTIONAL; + break; + case 3: // RequiredDependency + dependency.type = ModPlatform::DependencyType::REQUIRED; + break; + case 4: // Tool + dependency.type = ModPlatform::DependencyType::TOOL; + break; + case 5: // Incompatible + dependency.type = ModPlatform::DependencyType::INCOMPATIBLE; + break; + case 6: // Include + dependency.type = ModPlatform::DependencyType::INCLUDE; + break; + default: + dependency.type = ModPlatform::DependencyType::UNKNOWN; + break; + } + file.dependencies.append(dependency); + } + + if (load_changelog) file.changelog = api.getModFileChangelog(file.addonId.toInt(), file.fileId.toInt()); return file; } + +ModPlatform::IndexedVersion FlameMod::loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) +{ + QVector versions; + for (auto versionIter : arr) { + auto obj = versionIter.toObject(); + + auto file = loadIndexedPackVersion(obj); + if (!file.addonId.isValid()) + file.addonId = m.addonId; + + if (file.fileId.isValid()) // Heuristic to check if the returned value is valid + versions.append(file); + } + + auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool { + // dates are in RFC 3339 format + return a.date > b.date; + }; + std::sort(versions.begin(), versions.end(), orderSortPredicate); + return versions.front(); +}; diff --git a/launcher/modplatform/flame/FlameModIndex.h b/launcher/modplatform/flame/FlameModIndex.h index 33c4a5298..aa0d6f812 100644 --- a/launcher/modplatform/flame/FlameModIndex.h +++ b/launcher/modplatform/flame/FlameModIndex.h @@ -6,8 +6,8 @@ #include "modplatform/ModIndex.h" -#include "BaseInstance.h" #include +#include "BaseInstance.h" namespace FlameMod { @@ -19,5 +19,5 @@ void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, const shared_qobject_ptr& network, const BaseInstance* inst); auto loadIndexedPackVersion(QJsonObject& obj, bool load_changelog = false) -> ModPlatform::IndexedVersion; - -} // namespace FlameMod +auto loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion; +} // namespace FlameMod \ No newline at end of file diff --git a/launcher/modplatform/flame/FlamePackIndex.h b/launcher/modplatform/flame/FlamePackIndex.h index 1ca0fc0e5..b089b722c 100644 --- a/launcher/modplatform/flame/FlamePackIndex.h +++ b/launcher/modplatform/flame/FlamePackIndex.h @@ -4,6 +4,7 @@ #include #include #include +#include "modplatform/ModIndex.h" namespace Flame { @@ -27,8 +28,7 @@ struct ModpackExtra { QString sourceUrl; }; -struct IndexedPack -{ +struct IndexedPack { int addonId; QString name; QString description; @@ -43,9 +43,9 @@ struct IndexedPack ModpackExtra extra; }; -void loadIndexedPack(IndexedPack & m, QJsonObject & obj); +void loadIndexedPack(IndexedPack& m, QJsonObject& obj); void loadIndexedInfo(IndexedPack&, QJsonObject&); -void loadIndexedPackVersions(IndexedPack & m, QJsonArray & arr); -} +void loadIndexedPackVersions(IndexedPack& m, QJsonArray& arr); +} // namespace Flame Q_DECLARE_METATYPE(Flame::IndexedPack) diff --git a/launcher/modplatform/helpers/HashUtils.cpp b/launcher/modplatform/helpers/HashUtils.cpp index 81c94e1b5..6ff1d1710 100644 --- a/launcher/modplatform/helpers/HashUtils.cpp +++ b/launcher/modplatform/helpers/HashUtils.cpp @@ -71,6 +71,7 @@ void ModrinthHasher::executeTask() emitFailed("Empty hash!"); } else { emitSucceeded(); + emit resultsReady(m_hash); } } @@ -88,13 +89,13 @@ void FlameHasher::executeTask() emitFailed("Empty hash!"); } else { emitSucceeded(); + emit resultsReady(m_hash); } } - -BlockedModHasher::BlockedModHasher(QString file_path, ModPlatform::ResourceProvider provider) - : Hasher(file_path), provider(provider) { - setObjectName(QString("BlockedModHasher: %1").arg(file_path)); +BlockedModHasher::BlockedModHasher(QString file_path, ModPlatform::ResourceProvider provider) : Hasher(file_path), provider(provider) +{ + setObjectName(QString("BlockedModHasher: %1").arg(file_path)); hash_type = ProviderCaps.hashType(provider).first(); } @@ -120,14 +121,17 @@ void BlockedModHasher::executeTask() emitFailed("Empty hash!"); } else { emitSucceeded(); + emit resultsReady(m_hash); } } -QStringList BlockedModHasher::getHashTypes() { +QStringList BlockedModHasher::getHashTypes() +{ return ProviderCaps.hashType(provider); } -bool BlockedModHasher::useHashType(QString type) { +bool BlockedModHasher::useHashType(QString type) +{ auto types = ProviderCaps.hashType(provider); if (types.contains(type)) { hash_type = type; diff --git a/launcher/modplatform/helpers/HashUtils.h b/launcher/modplatform/helpers/HashUtils.h index 91146a525..73a2435a2 100644 --- a/launcher/modplatform/helpers/HashUtils.h +++ b/launcher/modplatform/helpers/HashUtils.h @@ -8,6 +8,7 @@ namespace Hashing { class Hasher : public Task { + Q_OBJECT public: using Ptr = shared_qobject_ptr; @@ -21,6 +22,9 @@ class Hasher : public Task { QString getResult() const { return m_hash; }; QString getPath() const { return m_path; }; + signals: + void resultsReady(QString hash); + protected: QString m_hash; QString m_path; @@ -48,6 +52,7 @@ class BlockedModHasher : public Hasher { QStringList getHashTypes(); bool useHashType(QString type); + private: ModPlatform::ResourceProvider provider; QString hash_type; diff --git a/launcher/modplatform/helpers/NetworkResourceAPI.cpp b/launcher/modplatform/helpers/NetworkResourceAPI.cpp index a3c592fdc..c278f800d 100644 --- a/launcher/modplatform/helpers/NetworkResourceAPI.cpp +++ b/launcher/modplatform/helpers/NetworkResourceAPI.cpp @@ -3,6 +3,7 @@ // SPDX-License-Identifier: GPL-3.0-only #include "NetworkResourceAPI.h" +#include #include "Application.h" #include "net/NetJob.h" @@ -19,12 +20,12 @@ Task::Ptr NetworkResourceAPI::searchProjects(SearchArgs&& args, SearchCallbacks& auto search_url = search_url_optional.value(); - auto response = new QByteArray(); + auto response = std::make_shared(); auto netJob = makeShared(QString("%1::Search").arg(debugName()), APPLICATION->network()); netJob->addNetAction(Net::Download::makeByteArray(QUrl(search_url), response)); - QObject::connect(netJob.get(), &NetJob::succeeded, [this, response, callbacks]{ + QObject::connect(netJob.get(), &NetJob::succeeded, [this, response, callbacks] { QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { @@ -40,27 +41,21 @@ Task::Ptr NetworkResourceAPI::searchProjects(SearchArgs&& args, SearchCallbacks& callbacks.on_succeed(doc); }); - QObject::connect(netJob.get(), &NetJob::failed, [&netJob, callbacks](QString reason){ + QObject::connect(netJob.get(), &NetJob::failed, [&netJob, callbacks](QString reason) { int network_error_code = -1; if (auto* failed_action = netJob->getFailedActions().at(0); failed_action && failed_action->m_reply) network_error_code = failed_action->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - callbacks.on_fail(reason, network_error_code); + callbacks.on_fail(reason, network_error_code); }); - QObject::connect(netJob.get(), &NetJob::aborted, [callbacks]{ - callbacks.on_abort(); - }); - QObject::connect(netJob.get(), &NetJob::finished, [response] { - delete response; - }); - + QObject::connect(netJob.get(), &NetJob::aborted, [callbacks] { callbacks.on_abort(); }); return netJob; } Task::Ptr NetworkResourceAPI::getProjectInfo(ProjectInfoArgs&& args, ProjectInfoCallbacks&& callbacks) const { - auto response = new QByteArray(); + auto response = std::make_shared(); auto job = getProject(args.pack.addonId.toString(), response); QObject::connect(job.get(), &NetJob::succeeded, [response, callbacks, args] { @@ -88,7 +83,7 @@ Task::Ptr NetworkResourceAPI::getProjectVersions(VersionSearchArgs&& args, Versi auto versions_url = versions_url_optional.value(); auto netJob = makeShared(QString("%1::Versions").arg(args.pack.name), APPLICATION->network()); - auto response = new QByteArray(); + auto response = std::make_shared(); netJob->addNetAction(Net::Download::makeByteArray(versions_url, response)); @@ -105,14 +100,10 @@ Task::Ptr NetworkResourceAPI::getProjectVersions(VersionSearchArgs&& args, Versi callbacks.on_succeed(doc, args.pack); }); - QObject::connect(netJob.get(), &NetJob::finished, [response] { - delete response; - }); - return netJob; } -Task::Ptr NetworkResourceAPI::getProject(QString addonId, QByteArray* response) const +Task::Ptr NetworkResourceAPI::getProject(QString addonId, std::shared_ptr response) const { auto project_url_optional = getInfoURL(addonId); if (!project_url_optional.has_value()) @@ -124,9 +115,34 @@ Task::Ptr NetworkResourceAPI::getProject(QString addonId, QByteArray* response) netJob->addNetAction(Net::Download::makeByteArray(QUrl(project_url), response)); - QObject::connect(netJob.get(), &NetJob::finished, [response] { - delete response; + return netJob; +} + +Task::Ptr NetworkResourceAPI::getDependencyVersion(DependencySearchArgs&& args, DependencySearchCallbacks&& callbacks) const +{ + auto versions_url_optional = getDependencyURL(args); + if (!versions_url_optional.has_value()) + return nullptr; + + auto versions_url = versions_url_optional.value(); + + auto netJob = makeShared(QString("%1::Dependency").arg(args.dependency.addonId.toString()), APPLICATION->network()); + auto response = std::make_shared(); + + netJob->addNetAction(Net::Download::makeByteArray(versions_url, response)); + + QObject::connect(netJob.get(), &NetJob::succeeded, [=] { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response for getting versions at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + return; + } + + callbacks.on_succeed(doc, args.dependency); }); return netJob; -} +}; diff --git a/launcher/modplatform/helpers/NetworkResourceAPI.h b/launcher/modplatform/helpers/NetworkResourceAPI.h index 94813bec8..b72e82533 100644 --- a/launcher/modplatform/helpers/NetworkResourceAPI.h +++ b/launcher/modplatform/helpers/NetworkResourceAPI.h @@ -4,19 +4,22 @@ #pragma once +#include #include "modplatform/ResourceAPI.h" class NetworkResourceAPI : public ResourceAPI { public: Task::Ptr searchProjects(SearchArgs&&, SearchCallbacks&&) const override; - Task::Ptr getProject(QString addonId, QByteArray* response) const override; + Task::Ptr getProject(QString addonId, std::shared_ptr response) const override; Task::Ptr getProjectInfo(ProjectInfoArgs&&, ProjectInfoCallbacks&&) const override; Task::Ptr getProjectVersions(VersionSearchArgs&&, VersionSearchCallbacks&&) const override; + Task::Ptr getDependencyVersion(DependencySearchArgs&&, DependencySearchCallbacks&&) const override; protected: [[nodiscard]] virtual auto getSearchURL(SearchArgs const& args) const -> std::optional = 0; [[nodiscard]] virtual auto getInfoURL(QString const& id) const -> std::optional = 0; [[nodiscard]] virtual auto getVersionsURL(VersionSearchArgs const& args) const -> std::optional = 0; + [[nodiscard]] virtual auto getDependencyURL(DependencySearchArgs const& args) const -> std::optional = 0; }; diff --git a/launcher/modplatform/legacy_ftb/PackFetchTask.cpp b/launcher/modplatform/legacy_ftb/PackFetchTask.cpp index e8768c5cd..a8a0fc2c2 100644 --- a/launcher/modplatform/legacy_ftb/PackFetchTask.cpp +++ b/launcher/modplatform/legacy_ftb/PackFetchTask.cpp @@ -51,11 +51,11 @@ void PackFetchTask::fetch() QUrl publicPacksUrl = QUrl(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/modpacks.xml"); qDebug() << "Downloading public version info from" << publicPacksUrl.toString(); - jobPtr->addNetAction(Net::Download::makeByteArray(publicPacksUrl, &publicModpacksXmlFileData)); + jobPtr->addNetAction(Net::Download::makeByteArray(publicPacksUrl, publicModpacksXmlFileData)); QUrl thirdPartyUrl = QUrl(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/thirdparty.xml"); qDebug() << "Downloading thirdparty version info from" << thirdPartyUrl.toString(); - jobPtr->addNetAction(Net::Download::makeByteArray(thirdPartyUrl, &thirdPartyModpacksXmlFileData)); + jobPtr->addNetAction(Net::Download::makeByteArray(thirdPartyUrl, thirdPartyModpacksXmlFileData)); QObject::connect(jobPtr.get(), &NetJob::succeeded, this, &PackFetchTask::fileDownloadFinished); QObject::connect(jobPtr.get(), &NetJob::failed, this, &PackFetchTask::fileDownloadFailed); @@ -64,22 +64,19 @@ void PackFetchTask::fetch() jobPtr->start(); } -void PackFetchTask::fetchPrivate(const QStringList & toFetch) +void PackFetchTask::fetchPrivate(const QStringList& toFetch) { QString privatePackBaseUrl = BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/%1.xml"; - for (auto &packCode: toFetch) - { - QByteArray *data = new QByteArray(); - NetJob *job = new NetJob("Fetching private pack", m_network); + for (auto& packCode : toFetch) { + auto data = std::make_shared(); + NetJob* job = new NetJob("Fetching private pack", m_network); job->addNetAction(Net::Download::makeByteArray(privatePackBaseUrl.arg(packCode), data)); - QObject::connect(job, &NetJob::succeeded, this, [this, job, data, packCode] - { + QObject::connect(job, &NetJob::succeeded, this, [this, job, data, packCode] { ModpackList packs; parseAndAddPacks(*data, PackType::Private, packs); - foreach(Modpack currentPack, packs) - { + foreach (Modpack currentPack, packs) { currentPack.packCode = packCode; emit privateFileDownloadFinished(currentPack); } @@ -87,24 +84,20 @@ void PackFetchTask::fetchPrivate(const QStringList & toFetch) job->deleteLater(); data->clear(); - delete data; }); - QObject::connect(job, &NetJob::failed, this, [this, job, packCode, data](QString reason) - { + QObject::connect(job, &NetJob::failed, this, [this, job, packCode, data](QString reason) { emit privateFileDownloadFailed(reason, packCode); job->deleteLater(); data->clear(); - delete data; }); - QObject::connect(job, &NetJob::aborted, this, [this, job, data]{ + QObject::connect(job, &NetJob::aborted, this, [this, job, data] { emit aborted(); job->deleteLater(); data->clear(); - delete data; }); job->start(); @@ -117,27 +110,22 @@ void PackFetchTask::fileDownloadFinished() QStringList failedLists; - if(!parseAndAddPacks(publicModpacksXmlFileData, PackType::Public, publicPacks)) - { + if (!parseAndAddPacks(*publicModpacksXmlFileData, PackType::Public, publicPacks)) { failedLists.append(tr("Public Packs")); } - if(!parseAndAddPacks(thirdPartyModpacksXmlFileData, PackType::ThirdParty, thirdPartyPacks)) - { + if (!parseAndAddPacks(*thirdPartyModpacksXmlFileData, PackType::ThirdParty, thirdPartyPacks)) { failedLists.append(tr("Third Party Packs")); } - if(failedLists.size() > 0) - { + if (failedLists.size() > 0) { emit failed(tr("Failed to download some pack lists: %1").arg(failedLists.join("\n- "))); - } - else - { + } else { emit finished(publicPacks, thirdPartyPacks); } } -bool PackFetchTask::parseAndAddPacks(QByteArray &data, PackType packType, ModpackList &list) +bool PackFetchTask::parseAndAddPacks(QByteArray& data, PackType packType, ModpackList& list) { QDomDocument doc; @@ -145,8 +133,7 @@ bool PackFetchTask::parseAndAddPacks(QByteArray &data, PackType packType, Modpac int errorLine = -1; int errorCol = -1; - if(!doc.setContent(data, false, &errorMsg, &errorLine, &errorCol)) - { + if (!doc.setContent(data, false, &errorMsg, &errorLine, &errorCol)) { auto fullErrMsg = QString("Failed to fetch modpack data: %1 %2:%3!").arg(errorMsg).arg(errorLine).arg(errorCol); qWarning() << fullErrMsg; data.clear(); @@ -154,8 +141,7 @@ bool PackFetchTask::parseAndAddPacks(QByteArray &data, PackType packType, Modpac } QDomNodeList nodes = doc.elementsByTagName("modpack"); - for(int i = 0; i < nodes.length(); i++) - { + for (int i = 0; i < nodes.length(); i++) { QDomElement element = nodes.at(i).toElement(); Modpack modpack; @@ -169,26 +155,20 @@ bool PackFetchTask::parseAndAddPacks(QByteArray &data, PackType packType, Modpac modpack.broken = false; modpack.bugged = false; - //remove empty if the xml is bugged - for(QString curr : modpack.oldVersions) - { - if(curr.isNull() || curr.isEmpty()) - { + // remove empty if the xml is bugged + for (QString curr : modpack.oldVersions) { + if (curr.isNull() || curr.isEmpty()) { modpack.oldVersions.removeAll(curr); modpack.bugged = true; qWarning() << "Removed some empty versions from" << modpack.name; } } - if(modpack.oldVersions.size() < 1) - { - if(!modpack.currentVersion.isNull() && !modpack.currentVersion.isEmpty()) - { + if (modpack.oldVersions.size() < 1) { + if (!modpack.currentVersion.isNull() && !modpack.currentVersion.isEmpty()) { modpack.oldVersions.append(modpack.currentVersion); qWarning() << "Added current version to oldVersions because oldVersions was empty! (" + modpack.name + ")"; - } - else - { + } else { modpack.broken = true; qWarning() << "Broken pack:" << modpack.name << " => No valid version!"; } @@ -218,4 +198,4 @@ void PackFetchTask::fileDownloadAborted() emit aborted(); } -} +} // namespace LegacyFTB diff --git a/launcher/modplatform/legacy_ftb/PackFetchTask.h b/launcher/modplatform/legacy_ftb/PackFetchTask.h index 8f3c4f3ba..f2116ce99 100644 --- a/launcher/modplatform/legacy_ftb/PackFetchTask.h +++ b/launcher/modplatform/legacy_ftb/PackFetchTask.h @@ -1,41 +1,41 @@ #pragma once -#include "net/NetJob.h" -#include #include #include +#include +#include #include "PackHelpers.h" +#include "net/NetJob.h" namespace LegacyFTB { class PackFetchTask : public QObject { - Q_OBJECT -public: - PackFetchTask(shared_qobject_ptr network) : QObject(nullptr), m_network(network) {}; + public: + PackFetchTask(shared_qobject_ptr network) : QObject(nullptr), m_network(network){}; virtual ~PackFetchTask() = default; void fetch(); - void fetchPrivate(const QStringList &toFetch); + void fetchPrivate(const QStringList& toFetch); -private: + private: shared_qobject_ptr m_network; NetJob::Ptr jobPtr; - QByteArray publicModpacksXmlFileData; - QByteArray thirdPartyModpacksXmlFileData; + std::shared_ptr publicModpacksXmlFileData = std::make_shared(); + std::shared_ptr thirdPartyModpacksXmlFileData = std::make_shared(); - bool parseAndAddPacks(QByteArray &data, PackType packType, ModpackList &list); + bool parseAndAddPacks(QByteArray& data, PackType packType, ModpackList& list); ModpackList publicPacks; ModpackList thirdPartyPacks; -protected slots: + protected slots: void fileDownloadFinished(); void fileDownloadFailed(QString reason); void fileDownloadAborted(); -signals: + signals: void finished(ModpackList publicPacks, ModpackList thirdPartyPacks); void failed(QString reason); void aborted(); @@ -44,4 +44,4 @@ signals: void privateFileDownloadFailed(QString reason, QString packCode); }; -} +} // namespace LegacyFTB diff --git a/launcher/modplatform/modrinth/ModrinthAPI.cpp b/launcher/modplatform/modrinth/ModrinthAPI.cpp index 29e3d129d..364cf3f30 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.cpp +++ b/launcher/modplatform/modrinth/ModrinthAPI.cpp @@ -9,19 +9,17 @@ #include "net/NetJob.h" #include "net/Upload.h" -Task::Ptr ModrinthAPI::currentVersion(QString hash, QString hash_format, QByteArray* response) +Task::Ptr ModrinthAPI::currentVersion(QString hash, QString hash_format, std::shared_ptr response) { auto netJob = makeShared(QString("Modrinth::GetCurrentVersion"), APPLICATION->network()); netJob->addNetAction(Net::Download::makeByteArray( QString(BuildConfig.MODRINTH_PROD_URL + "/version_file/%1?algorithm=%2").arg(hash, hash_format), response)); - QObject::connect(netJob.get(), &NetJob::finished, [response] { delete response; }); - return netJob; } -Task::Ptr ModrinthAPI::currentVersions(const QStringList& hashes, QString hash_format, QByteArray* response) +Task::Ptr ModrinthAPI::currentVersions(const QStringList& hashes, QString hash_format, std::shared_ptr response) { auto netJob = makeShared(QString("Modrinth::GetCurrentVersions"), APPLICATION->network()); @@ -35,8 +33,6 @@ Task::Ptr ModrinthAPI::currentVersions(const QStringList& hashes, QString hash_f netJob->addNetAction(Net::Upload::makeByteArray(QString(BuildConfig.MODRINTH_PROD_URL + "/version_files"), response, body_raw)); - QObject::connect(netJob.get(), &NetJob::finished, [response] { delete response; }); - return netJob; } @@ -44,7 +40,7 @@ Task::Ptr ModrinthAPI::latestVersion(QString hash, QString hash_format, std::optional> mcVersions, std::optional loaders, - QByteArray* response) + std::shared_ptr response) { auto netJob = makeShared(QString("Modrinth::GetLatestVersion"), APPLICATION->network()); @@ -67,8 +63,6 @@ Task::Ptr ModrinthAPI::latestVersion(QString hash, netJob->addNetAction(Net::Upload::makeByteArray( QString(BuildConfig.MODRINTH_PROD_URL + "/version_file/%1/update?algorithm=%2").arg(hash, hash_format), response, body_raw)); - QObject::connect(netJob.get(), &NetJob::finished, [response] { delete response; }); - return netJob; } @@ -76,7 +70,7 @@ Task::Ptr ModrinthAPI::latestVersions(const QStringList& hashes, QString hash_format, std::optional> mcVersions, std::optional loaders, - QByteArray* response) + std::shared_ptr response) { auto netJob = makeShared(QString("Modrinth::GetLatestVersions"), APPLICATION->network()); @@ -101,22 +95,16 @@ Task::Ptr ModrinthAPI::latestVersions(const QStringList& hashes, netJob->addNetAction(Net::Upload::makeByteArray(QString(BuildConfig.MODRINTH_PROD_URL + "/version_files/update"), response, body_raw)); - QObject::connect(netJob.get(), &NetJob::finished, [response] { delete response; }); - return netJob; } -Task::Ptr ModrinthAPI::getProjects(QStringList addonIds, QByteArray* response) const +Task::Ptr ModrinthAPI::getProjects(QStringList addonIds, std::shared_ptr response) const { auto netJob = makeShared(QString("Modrinth::GetProjects"), APPLICATION->network()); auto searchUrl = getMultipleModInfoURL(addonIds); netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), response)); - QObject::connect(netJob.get(), &NetJob::finished, [response, netJob] { - delete response; - }); - return netJob; } diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index b91ac5c14..e83ed2bf7 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -12,27 +12,23 @@ class ModrinthAPI : public NetworkResourceAPI { public: - auto currentVersion(QString hash, - QString hash_format, - QByteArray* response) -> Task::Ptr; + auto currentVersion(QString hash, QString hash_format, std::shared_ptr response) -> Task::Ptr; - auto currentVersions(const QStringList& hashes, - QString hash_format, - QByteArray* response) -> Task::Ptr; + auto currentVersions(const QStringList& hashes, QString hash_format, std::shared_ptr response) -> Task::Ptr; auto latestVersion(QString hash, QString hash_format, std::optional> mcVersions, std::optional loaders, - QByteArray* response) -> Task::Ptr; + std::shared_ptr response) -> Task::Ptr; auto latestVersions(const QStringList& hashes, QString hash_format, - std::optional> mcVersions, - std::optional loaders, - QByteArray* response) -> Task::Ptr; + std::optional> mcVersions, + std::optional loaders, + std::shared_ptr response) -> Task::Ptr; - Task::Ptr getProjects(QStringList addonIds, QByteArray* response) const override; + Task::Ptr getProjects(QStringList addonIds, std::shared_ptr response) const override; public: [[nodiscard]] auto getSortingMethods() const -> QList override; @@ -42,7 +38,7 @@ class ModrinthAPI : public NetworkResourceAPI { static auto getModLoaderStrings(const ModLoaderTypes types) -> const QStringList { QStringList l; - for (auto loader : {Forge, Fabric, Quilt}) { + for (auto loader : { Forge, Fabric, Quilt }) { if (types & loader) { l << getModLoaderString(loader); } @@ -55,8 +51,7 @@ class ModrinthAPI : public NetworkResourceAPI { static auto getModLoaderFilters(ModLoaderTypes types) -> const QString { QStringList l; - for (auto loader : getModLoaderStrings(types)) - { + for (auto loader : getModLoaderStrings(types)) { l << QString("\"categories:%1\"").arg(loader); } return l.join(','); @@ -139,16 +134,22 @@ class ModrinthAPI : public NetworkResourceAPI { auto getGameVersionsArray(std::list mcVersions) const -> QString { QString s; - for(auto& ver : mcVersions){ + for (auto& ver : mcVersions) { s += QString("\"versions:%1\",").arg(ver.toString()); } - s.remove(s.length() - 1, 1); //remove last comma + s.remove(s.length() - 1, 1); // remove last comma return s.isEmpty() ? QString() : s; } - inline auto validateModLoaders(ModLoaderTypes loaders) const -> bool - { - return loaders & (Forge | Fabric | Quilt); - } + inline auto validateModLoaders(ModLoaderTypes loaders) const -> bool { return loaders & (Forge | Fabric | Quilt); } + [[nodiscard]] std::optional getDependencyURL(DependencySearchArgs const& args) const override + { + return args.dependency.version.length() != 0 ? QString("%1/version/%2").arg(BuildConfig.MODRINTH_PROD_URL, args.dependency.version) + : QString("%1/project/%2/version?game_versions=[\"%3\"]&loaders=[\"%4\"]") + .arg(BuildConfig.MODRINTH_PROD_URL) + .arg(args.dependency.addonId.toString()) + .arg(args.mcVersion.toString()) + .arg(getModLoaderStrings(args.loader).join("\",\"")); + }; }; diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp index d1be72099..a7c22832a 100644 --- a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp +++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp @@ -53,12 +53,11 @@ void ModrinthCheckUpdate::executeTask() // (though it will rarely happen, if at all) if (mod->metadata()->hash_format != best_hash_type) { auto hash_task = Hashing::createModrinthHasher(mod->fileinfo().absoluteFilePath()); - connect(hash_task.get(), &Task::succeeded, [&] { - QString hash (hash_task->getResult()); + connect(hash_task.get(), &Hashing::Hasher::resultsReady, [&hashes, &mappings, mod](QString hash) { hashes.append(hash); mappings.insert(hash, mod); }); - connect(hash_task.get(), &Task::failed, [this, hash_task] { failed("Failed to generate hash"); }); + connect(hash_task.get(), &Task::failed, [this] { failed("Failed to generate hash"); }); hashing_task.addTask(hash_task); } else { hashes.append(hash); @@ -67,11 +66,11 @@ void ModrinthCheckUpdate::executeTask() } QEventLoop loop; - connect(&hashing_task, &Task::finished, [&loop]{ loop.quit(); }); + connect(&hashing_task, &Task::finished, [&loop] { loop.quit(); }); hashing_task.start(); loop.exec(); - auto* response = new QByteArray(); + auto response = std::make_shared(); auto job = api.latestVersions(hashes, best_hash_type, m_game_versions, m_loaders, response); QEventLoop lock; @@ -112,7 +111,8 @@ void ModrinthCheckUpdate::executeTask() // so we may want to filter it QString loader_filter; if (m_loaders.has_value()) { - static auto flags = { ResourceAPI::ModLoaderType::Forge, ResourceAPI::ModLoaderType::Fabric, ResourceAPI::ModLoaderType::Quilt }; + static auto flags = { ResourceAPI::ModLoaderType::Forge, ResourceAPI::ModLoaderType::Fabric, + ResourceAPI::ModLoaderType::Quilt }; for (auto flag : flags) { if (m_loaders.value().testFlag(flag)) { loader_filter = api.getModLoaderString(flag); @@ -122,7 +122,8 @@ void ModrinthCheckUpdate::executeTask() } // Currently, we rely on a couple heuristics to determine whether an update is actually available or not: - // - The file needs to be preferred: It is either the primary file, or the one found via (explicit) usage of the loader_filter + // - The file needs to be preferred: It is either the primary file, or the one found via (explicit) usage of the + // loader_filter // - The version reported by the JAR is different from the version reported by the indexed version (it's usually the case) // Such is the pain of having arbitrary files for a given version .-. @@ -149,19 +150,19 @@ void ModrinthCheckUpdate::executeTask() continue; // Fake pack with the necessary info to pass to the download task :) - ModPlatform::IndexedPack pack; - pack.name = mod->name(); - pack.slug = mod->metadata()->slug; - pack.addonId = mod->metadata()->project_id; - pack.websiteUrl = mod->homeurl(); + auto pack = std::make_shared(); + 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(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 bb8227aa2..76f072773 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 000000000..4cd88aa69 --- /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 + * + * 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 . + */ + +#include "ModrinthPackExportTask.h" + +#include +#include +#include +#include +#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(instance.get())) + , gameRoot(instance->gameRoot()) + , output(output) + , filter(filter) +{} + +void ModrinthPackExportTask::executeTask() +{ + setStatus(tr("Searching for files...")); + setProgress(0, 0); + collectFiles(); +} + +bool ModrinthPackExportTask::abort() +{ + if (task != nullptr) { + task->abort(); + task = nullptr; + emitAborted(); + return true; + } + + if (buildZipFuture.isRunning()) { + buildZipFuture.cancel(); + // NOTE: Here we don't do `emitAborted()` because it will be done when `buildZipFuture` actually cancels, which may not occur immediately. + return true; + } + + return false; +} + +void ModrinthPackExportTask::collectFiles() +{ + setAbortable(false); + QCoreApplication::processEvents(); + + files.clear(); + if (!MMCZip::collectFileListRecursively(instance->gameRoot(), nullptr, &files, filter)) { + emitFailed(tr("Could not search for files")); + return; + } + + pendingHashes.clear(); + resolvedFiles.clear(); + + if (mcInstance) { + mcInstance->loaderModList()->update(); + connect(mcInstance->loaderModList().get(), &ModFolderModel::updateFinished, this, &ModrinthPackExportTask::collectHashes); + } else + collectHashes(); +} + +void ModrinthPackExportTask::collectHashes() +{ + for (const QFileInfo& file : files) { + QCoreApplication::processEvents(); + + const QString relative = gameRoot.relativeFilePath(file.absoluteFilePath()); + // require sensible file types + if (!std::any_of(PREFIXES.begin(), PREFIXES.end(), [&relative](const QString& prefix) { return relative.startsWith(prefix); })) + continue; + if (!std::any_of(FILE_EXTENSIONS.begin(), FILE_EXTENSIONS.end(), [&relative](const QString& extension) { + return relative.endsWith('.' + extension) || relative.endsWith('.' + extension + ".disabled"); + })) + continue; + + QCryptographicHash sha512(QCryptographicHash::Algorithm::Sha512); + + QFile openFile(file.absoluteFilePath()); + if (!openFile.open(QFile::ReadOnly)) { + qWarning() << "Could not open" << file << "for hashing"; + continue; + } + + const QByteArray data = openFile.readAll(); + if (openFile.error() != QFileDevice::NoError) { + qWarning() << "Could not read" << file; + continue; + } + sha512.addData(data); + + auto allMods = mcInstance->loaderModList()->allMods(); + if (auto modIter = std::find_if(allMods.begin(), allMods.end(), [&file](Mod* mod) { return mod->fileinfo() == file; }); + modIter != allMods.end()) { + const Mod* mod = *modIter; + if (mod->metadata() != nullptr) { + QUrl& url = mod->metadata()->url; + // ensure the url is permitted on modrinth.com + if (!url.isEmpty() && BuildConfig.MODRINTH_MRPACK_HOSTS.contains(url.host())) { + qDebug() << "Resolving" << relative << "from index"; + + QCryptographicHash sha1(QCryptographicHash::Algorithm::Sha1); + sha1.addData(data); + + ResolvedFile resolvedFile{ sha1.result().toHex(), sha512.result().toHex(), url.toEncoded(), openFile.size() }; + resolvedFiles[relative] = resolvedFile; + + // nice! we've managed to resolve based on local metadata! + // no need to enqueue it + continue; + } + } + } + + qDebug() << "Enqueueing" << relative << "for Modrinth query"; + pendingHashes[relative] = sha512.result().toHex(); + } + + setAbortable(true); + makeApiRequest(); +} + +void ModrinthPackExportTask::makeApiRequest() +{ + if (pendingHashes.isEmpty()) + buildZip(); + else { + auto response = std::make_shared(); + task = api.currentVersions(pendingHashes.values(), "sha512", response); + connect(task.get(), &NetJob::succeeded, [this, response]() { parseApiResponse(response); }); + connect(task.get(), &NetJob::failed, this, &ModrinthPackExportTask::emitFailed); + task->start(); + } +} + +void ModrinthPackExportTask::parseApiResponse(const std::shared_ptr response) +{ + task = nullptr; + + try { + const QJsonDocument doc = Json::requireDocument(*response); + + QMapIterator 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::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 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 000000000..96f292c1b --- /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 + * + * 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 . + */ + +#pragma once + +#include +#include +#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 BuildZipResult; + + ModrinthAPI api; + QFileInfoList files; + QMap pendingHashes; + QMap resolvedFiles; + Task::Ptr task; + QFuture buildZipFuture; + QFutureWatcher buildZipWatcher; + + void collectFiles(); + void collectHashes(); + void makeApiRequest(); + void parseApiResponse(const std::shared_ptr response); + void buildZip(); + void finish(); + + QByteArray generateIndex(); +}; diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp index 7ade131e4..b40373496 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp @@ -22,7 +22,7 @@ #include "Json.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" -#include "net/NetJob.h" +#include "modplatform/ModIndex.h" static ModrinthAPI api; static ModPlatform::ProviderCapabilities ProviderCaps; @@ -140,6 +140,28 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject& obj, QString preferred_hash_t file.version_number = Json::requireString(obj, "version_number"); file.changelog = Json::requireString(obj, "changelog"); + auto dependencies = Json::ensureArray(obj, "dependencies"); + for (auto d : dependencies) { + auto dep = Json::ensureObject(d); + ModPlatform::Dependency dependency; + dependency.addonId = Json::ensureString(dep, "project_id"); + dependency.version = Json::ensureString(dep, "version_id"); + auto depType = Json::requireString(dep, "dependency_type"); + + if (depType == "required") + dependency.type = ModPlatform::DependencyType::REQUIRED; + else if (depType == "optional") + dependency.type = ModPlatform::DependencyType::OPTIONAL; + else if (depType == "incompatible") + dependency.type = ModPlatform::DependencyType::INCOMPATIBLE; + else if (depType == "embedded") + dependency.type = ModPlatform::DependencyType::EMBEDDED; + else + dependency.type = ModPlatform::DependencyType::UNKNOWN; + + file.dependencies.append(dependency); + } + auto files = Json::requireArray(obj, "files"); int i = 0; @@ -195,3 +217,22 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject& obj, QString preferred_hash_t return {}; } + +auto Modrinth::loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion +{ + QVector versions; + + for (auto versionIter : arr) { + auto obj = versionIter.toObject(); + auto file = loadIndexedPackVersion(obj); + + if (file.fileId.isValid()) // Heuristic to check if the returned value is valid + versions.append(file); + } + auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool { + // dates are in RFC 3339 format + return a.date > b.date; + }; + std::sort(versions.begin(), versions.end(), orderSortPredicate); + return versions.length() != 0 ? versions.front() : ModPlatform::IndexedVersion(); +} \ No newline at end of file diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.h b/launcher/modplatform/modrinth/ModrinthPackIndex.h index e73e4b186..a8d986c57 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.h +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.h @@ -19,8 +19,8 @@ #include "modplatform/ModIndex.h" -#include "BaseInstance.h" #include +#include "BaseInstance.h" namespace Modrinth { @@ -31,5 +31,6 @@ void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, const shared_qobject_ptr& network, const BaseInstance* inst); auto loadIndexedPackVersion(QJsonObject& obj, QString hash_type = "sha512", QString filename_prefer = "") -> ModPlatform::IndexedVersion; +auto loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion; } // namespace Modrinth diff --git a/launcher/modplatform/technic/SolderPackInstallTask.cpp b/launcher/modplatform/technic/SolderPackInstallTask.cpp index c26d6a5a6..6a05d17ae 100644 --- a/launcher/modplatform/technic/SolderPackInstallTask.cpp +++ b/launcher/modplatform/technic/SolderPackInstallTask.cpp @@ -37,20 +37,19 @@ #include #include -#include #include +#include -#include "TechnicPackProcessor.h" #include "SolderPackManifest.h" +#include "TechnicPackProcessor.h" #include "net/ChecksumValidator.h" -Technic::SolderPackInstallTask::SolderPackInstallTask( - shared_qobject_ptr network, - const QUrl &solderUrl, - const QString &pack, - const QString &version, - const QString &minecraftVersion -) { +Technic::SolderPackInstallTask::SolderPackInstallTask(shared_qobject_ptr network, + const QUrl& solderUrl, + const QString& pack, + const QString& version, + const QString& minecraftVersion) +{ m_solderUrl = solderUrl; m_pack = pack; m_version = version; @@ -58,9 +57,9 @@ Technic::SolderPackInstallTask::SolderPackInstallTask( m_minecraftVersion = minecraftVersion; } -bool Technic::SolderPackInstallTask::abort() { - if(m_abortable) - { +bool Technic::SolderPackInstallTask::abort() +{ + if (m_abortable) { return m_filesNetJob->abort(); } return false; @@ -72,7 +71,7 @@ void Technic::SolderPackInstallTask::executeTask() m_filesNetJob.reset(new NetJob(tr("Resolving modpack files"), m_network)); auto sourceUrl = QString("%1/modpack/%2/%3").arg(m_solderUrl.toString(), m_pack, m_version); - m_filesNetJob->addNetAction(Net::Download::makeByteArray(sourceUrl, &m_response)); + m_filesNetJob->addNetAction(Net::Download::makeByteArray(sourceUrl, m_response)); auto job = m_filesNetJob.get(); connect(job, &NetJob::succeeded, this, &Technic::SolderPackInstallTask::fileListSucceeded); @@ -85,11 +84,11 @@ void Technic::SolderPackInstallTask::fileListSucceeded() { setStatus(tr("Downloading modpack")); - QJsonParseError parse_error {}; - QJsonDocument doc = QJsonDocument::fromJson(m_response, &parse_error); + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*m_response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from Solder at " << parse_error.offset << " reason: " << parse_error.errorString(); - qWarning() << m_response; + qWarning() << *m_response; return; } auto obj = doc.object(); @@ -110,7 +109,7 @@ void Technic::SolderPackInstallTask::fileListSucceeded() m_filesNetJob.reset(new NetJob(tr("Downloading modpack"), m_network)); int i = 0; - for (const auto &mod : build.mods) { + for (const auto& mod : build.mods) { auto path = FS::PathCombine(m_outputDir.path(), QString("%1").arg(i)); auto dl = Net::Download::makeFile(mod.url, path); diff --git a/launcher/modplatform/technic/SolderPackInstallTask.h b/launcher/modplatform/technic/SolderPackInstallTask.h index aa14ce88b..f2c6a83a4 100644 --- a/launcher/modplatform/technic/SolderPackInstallTask.h +++ b/launcher/modplatform/technic/SolderPackInstallTask.h @@ -40,45 +40,48 @@ #include #include +#include -namespace Technic -{ - class SolderPackInstallTask : public InstanceTask - { - Q_OBJECT - public: - explicit SolderPackInstallTask(shared_qobject_ptr network, const QUrl &solderUrl, const QString& pack, const QString& version, const QString &minecraftVersion); +namespace Technic { +class SolderPackInstallTask : public InstanceTask { + Q_OBJECT + public: + explicit SolderPackInstallTask(shared_qobject_ptr network, + const QUrl& solderUrl, + const QString& pack, + const QString& version, + const QString& minecraftVersion); - bool canAbort() const override { return true; } - bool abort() override; + bool canAbort() const override { return true; } + bool abort() override; - protected: - //! Entry point for tasks. - virtual void executeTask() override; + protected: + //! Entry point for tasks. + virtual void executeTask() override; - private slots: - void fileListSucceeded(); - void downloadSucceeded(); - void downloadFailed(QString reason); - void downloadProgressChanged(qint64 current, qint64 total); - void downloadAborted(); - void extractFinished(); - void extractAborted(); + private slots: + void fileListSucceeded(); + void downloadSucceeded(); + void downloadFailed(QString reason); + void downloadProgressChanged(qint64 current, qint64 total); + void downloadAborted(); + void extractFinished(); + void extractAborted(); - private: - bool m_abortable = false; + private: + bool m_abortable = false; - shared_qobject_ptr m_network; + shared_qobject_ptr m_network; - NetJob::Ptr m_filesNetJob; - QUrl m_solderUrl; - QString m_pack; - QString m_version; - QString m_minecraftVersion; - QByteArray m_response; - QTemporaryDir m_outputDir; - int m_modCount; - QFuture m_extractFuture; - QFutureWatcher m_extractFutureWatcher; - }; -} + NetJob::Ptr m_filesNetJob; + QUrl m_solderUrl; + QString m_pack; + QString m_version; + QString m_minecraftVersion; + std::shared_ptr m_response = std::make_shared(); + QTemporaryDir m_outputDir; + int m_modCount; + QFuture m_extractFuture; + QFutureWatcher m_extractFutureWatcher; +}; +} // namespace Technic diff --git a/launcher/mojang/PackageManifest.cpp b/launcher/mojang/PackageManifest.cpp deleted file mode 100644 index b3dfd7fc1..000000000 --- a/launcher/mojang/PackageManifest.cpp +++ /dev/null @@ -1,427 +0,0 @@ -#include "PackageManifest.h" -#include -#include -#include -#include -#include - -#ifndef Q_OS_WIN32 -#include -#include -#include -#endif - -namespace mojang_files { - -const Hash hash_of_empty_string = "da39a3ee5e6b4b0d3255bfef95601890afd80709"; - -int Path::compare(const Path& rhs) const -{ - auto left_cursor = begin(); - auto left_end = end(); - auto right_cursor = rhs.begin(); - auto right_end = rhs.end(); - - while (left_cursor != left_end && right_cursor != right_end) - { - if(*left_cursor < *right_cursor) - { - return -1; - } - else if(*left_cursor > *right_cursor) - { - return 1; - } - left_cursor++; - right_cursor++; - } - - if(left_cursor == left_end) - { - if(right_cursor == right_end) - { - return 0; - } - return -1; - } - return 1; -} - -void Package::addFile(const Path& path, const File& file) { - addFolder(path.parent_path()); - files[path] = file; -} - -void Package::addFolder(Path folder) { - if(!folder.has_parent_path()) { - return; - } - do { - folders.insert(folder); - folder = folder.parent_path(); - } while(folder.has_parent_path()); -} - -void Package::addLink(const Path& path, const Path& target) { - addFolder(path.parent_path()); - symlinks[path] = target; -} - -void Package::addSource(const FileSource& source) { - sources[source.hash] = source; -} - - -namespace { -void fromJson(QJsonDocument & doc, Package & out) { - std::set seen_paths; - if (!doc.isObject()) - { - throw JSONValidationError("file manifest is not an object"); - } - QJsonObject root = doc.object(); - - auto filesObj = Json::ensureObject(root, "files"); - auto iter = filesObj.begin(); - while (iter != filesObj.end()) - { - Path objectPath = Path(iter.key()); - auto value = iter.value(); - iter++; - if(seen_paths.count(objectPath)) { - throw JSONValidationError("duplicate path inside manifest, the manifest is invalid"); - } - if (!value.isObject()) - { - throw JSONValidationError("file entry inside manifest is not an an object"); - } - seen_paths.insert(objectPath); - - auto fileObject = value.toObject(); - auto type = Json::requireString(fileObject, "type"); - if(type == "directory") { - out.addFolder(objectPath); - continue; - } - else if(type == "file") { - FileSource bestSource; - File file; - file.executable = Json::ensureBoolean(fileObject, QString("executable"), false); - auto downloads = Json::requireObject(fileObject, "downloads"); - for(auto iter2 = downloads.begin(); iter2 != downloads.end(); iter2++) { - FileSource source; - - auto downloadObject = Json::requireObject(iter2.value()); - source.hash = Json::requireString(downloadObject, "sha1"); - source.size = Json::requireInteger(downloadObject, "size"); - source.url = Json::requireString(downloadObject, "url"); - - auto compression = iter2.key(); - if(compression == "raw") { - file.hash = source.hash; - file.size = source.size; - source.compression = Compression::Raw; - } - else if (compression == "lzma") { - source.compression = Compression::Lzma; - } - else { - continue; - } - bestSource.upgrade(source); - } - if(bestSource.isBad()) { - throw JSONValidationError("No valid compression method for file " + iter.key()); - } - out.addFile(objectPath, file); - out.addSource(bestSource); - } - else if(type == "link") { - auto target = Json::requireString(fileObject, "target"); - out.symlinks[objectPath] = target; - out.addLink(objectPath, target); - } - else { - throw JSONValidationError("Invalid item type in manifest: " + type); - } - } - // make sure the containing folder exists - out.folders.insert(Path()); -} -} - -Package Package::fromManifestContents(const QByteArray& contents) -{ - Package out; - try - { - auto doc = Json::requireDocument(contents, "Manifest"); - fromJson(doc, out); - return out; - } - catch (const Exception &e) - { - qDebug() << QString("Unable to parse manifest: %1").arg(e.cause()); - out.valid = false; - return out; - } -} - -Package Package::fromManifestFile(const QString & filename) { - Package out; - try - { - auto doc = Json::requireDocument(filename, filename); - fromJson(doc, out); - return out; - } - catch (const Exception &e) - { - qDebug() << QString("Unable to parse manifest file %1: %2").arg(filename, e.cause()); - out.valid = false; - return out; - } -} - -#ifndef Q_OS_WIN32 - -#include -#include -#include - -namespace { -// FIXME: Qt obscures symlink targets by making them absolute. that is useless. this is the workaround - we do it ourselves -bool actually_read_symlink_target(const QString & filepath, Path & out) -{ - struct ::stat st; - // FIXME: here, we assume the native filesystem encoding. May the Gods have mercy upon our Souls. - QByteArray nativePath = filepath.toUtf8(); - const char * filepath_cstr = nativePath.data(); - - if (lstat(filepath_cstr, &st) != 0) - { - return false; - } - - auto size = st.st_size ? st.st_size + 1 : PATH_MAX; - std::string temp(size, '\0'); - // because we don't realiably know how long the damn thing actually is, we loop and expand. POSIX is naff - do - { - auto link_length = ::readlink(filepath_cstr, &temp[0], temp.size()); - if(link_length == -1) - { - return false; - } - if(std::string::size_type(link_length) < temp.size()) - { - // buffer was long enough and we managed to read the link target. RETURN here. - temp.resize(link_length); - out = Path(QString::fromUtf8(temp.c_str())); - return true; - } - temp.resize(temp.size() * 2); - } while (true); -} -} -#endif - -// FIXME: Qt filesystem abstraction is bad, but ... let's hope it doesn't break too much? -// FIXME: The error handling is just DEFICIENT -Package Package::fromInspectedFolder(const QString& folderPath) -{ - QDir root(folderPath); - - Package out; - QDirIterator iterator(folderPath, QDir::NoDotAndDotDot | QDir::AllEntries | QDir::System | QDir::Hidden, QDirIterator::Subdirectories); - while(iterator.hasNext()) { - iterator.next(); - - auto fileInfo = iterator.fileInfo(); - auto relPath = root.relativeFilePath(fileInfo.filePath()); - // FIXME: this is probably completely busted on Windows anyway, so just disable it. - // Qt makes shit up and doesn't understand the platform details - // TODO: Actually use a filesystem library that isn't terrible and has decen license. - // I only know one, and I wrote it. Sadly, currently proprietary. PAIN. -#ifndef Q_OS_WIN32 - if(fileInfo.isSymLink()) { - Path targetPath; - if(!actually_read_symlink_target(fileInfo.filePath(), targetPath)) { - qCritical() << "Folder inspection: Unknown filesystem object:" << fileInfo.absoluteFilePath(); - out.valid = false; - } - out.addLink(relPath, targetPath); - } - else -#endif - if(fileInfo.isDir()) { - out.addFolder(relPath); - } - else if(fileInfo.isFile()) { - File f; - f.executable = fileInfo.isExecutable(); - f.size = fileInfo.size(); - // FIXME: async / optimize the hashing - QFile input(fileInfo.absoluteFilePath()); - if(!input.open(QIODevice::ReadOnly)) { - qCritical() << "Folder inspection: Failed to open file:" << fileInfo.absoluteFilePath(); - out.valid = false; - break; - } - f.hash = QCryptographicHash::hash(input.readAll(), QCryptographicHash::Sha1).toHex().constData(); - out.addFile(relPath, f); - } - else { - // Something else... oh my - qCritical() << "Folder inspection: Unknown filesystem object:" << fileInfo.absoluteFilePath(); - out.valid = false; - break; - } - } - out.folders.insert(Path(".")); - out.valid = true; - return out; -} - -namespace { -struct shallow_first_sort -{ - bool operator()(const Path &lhs, const Path &rhs) const - { - auto lhs_depth = lhs.length(); - auto rhs_depth = rhs.length(); - if(lhs_depth < rhs_depth) - { - return true; - } - else if(lhs_depth == rhs_depth) - { - if(lhs < rhs) - { - return true; - } - } - return false; - } -}; - -struct deep_first_sort -{ - bool operator()(const Path &lhs, const Path &rhs) const - { - auto lhs_depth = lhs.length(); - auto rhs_depth = rhs.length(); - if(lhs_depth > rhs_depth) - { - return true; - } - else if(lhs_depth == rhs_depth) - { - if(lhs < rhs) - { - return true; - } - } - return false; - } -}; -} - -UpdateOperations UpdateOperations::resolve(const Package& from, const Package& to) -{ - UpdateOperations out; - - if(!from.valid || !to.valid) { - out.valid = false; - return out; - } - - // Files - for(auto iter = from.files.begin(); iter != from.files.end(); iter++) { - const auto ¤t_hash = iter->second.hash; - const auto ¤t_executable = iter->second.executable; - const auto &path = iter->first; - - auto iter2 = to.files.find(path); - if(iter2 == to.files.end()) { - // removed - out.deletes.push_back(path); - continue; - } - auto new_hash = iter2->second.hash; - auto new_executable = iter2->second.executable; - if (current_hash != new_hash) { - out.deletes.push_back(path); - out.downloads.emplace( - std::pair{ - path, - FileDownload(to.sources.at(iter2->second.hash), iter2->second.executable) - } - ); - } - else if (current_executable != new_executable) { - out.executable_fixes[path] = new_executable; - } - } - for(auto iter = to.files.begin(); iter != to.files.end(); iter++) { - auto path = iter->first; - if(!from.files.count(path)) { - out.downloads.emplace( - std::pair{ - path, - FileDownload(to.sources.at(iter->second.hash), iter->second.executable) - } - ); - } - } - - // Folders - std::set remove_folders; - std::set make_folders; - for(auto from_path: from.folders) { - auto iter = to.folders.find(from_path); - if(iter == to.folders.end()) { - remove_folders.insert(from_path); - } - } - for(auto & rmdir: remove_folders) { - out.rmdirs.push_back(rmdir); - } - for(auto to_path: to.folders) { - auto iter = from.folders.find(to_path); - if(iter == from.folders.end()) { - make_folders.insert(to_path); - } - } - for(auto & mkdir: make_folders) { - out.mkdirs.push_back(mkdir); - } - - // Symlinks - for(auto iter = from.symlinks.begin(); iter != from.symlinks.end(); iter++) { - const auto ¤t_target = iter->second; - const auto &path = iter->first; - - auto iter2 = to.symlinks.find(path); - if(iter2 == to.symlinks.end()) { - // removed - out.deletes.push_back(path); - continue; - } - const auto &new_target = iter2->second; - if (current_target != new_target) { - out.deletes.push_back(path); - out.mklinks[path] = iter2->second; - } - } - for(auto iter = to.symlinks.begin(); iter != to.symlinks.end(); iter++) { - auto path = iter->first; - if(!from.symlinks.count(path)) { - out.mklinks[path] = iter->second; - } - } - out.valid = true; - return out; -} - -} diff --git a/launcher/mojang/PackageManifest.h b/launcher/mojang/PackageManifest.h deleted file mode 100644 index fd7ab0ad4..000000000 --- a/launcher/mojang/PackageManifest.h +++ /dev/null @@ -1,171 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include "tasks/Task.h" - -namespace mojang_files { - -using Hash = QString; -extern const Hash empty_hash; - -// simple-ish path implementation. assumes always relative and does not allow '..' entries -class Path -{ -public: - using parts_type = QStringList; - - Path() = default; - Path(QString string) { - auto parts_in = string.split('/'); - for(auto & part: parts_in) { - if(part.isEmpty() || part == ".") { - continue; - } - if(part == "..") { - if(parts.size()) { - parts.pop_back(); - } - continue; - } - parts.push_back(part); - } - } - - bool has_parent_path() const - { - return parts.size() > 0; - } - - Path parent_path() const - { - if (parts.empty()) - return Path(); - return Path(parts.begin(), std::prev(parts.end())); - } - - bool empty() const - { - return parts.empty(); - } - - int length() const - { - return parts.length(); - } - - bool operator==(const Path & rhs) const { - return parts == rhs.parts; - } - - bool operator!=(const Path & rhs) const { - return parts != rhs.parts; - } - - inline bool operator<(const Path& rhs) const - { - return compare(rhs) < 0; - } - - parts_type::const_iterator begin() const - { - return parts.begin(); - } - - parts_type::const_iterator end() const - { - return parts.end(); - } - - QString toString() const { - return parts.join("/"); - } - -private: - Path(const parts_type::const_iterator & start, const parts_type::const_iterator & end) { - auto cursor = start; - while(cursor != end) { - parts.push_back(*cursor); - cursor++; - } - } - int compare(const Path& p) const; - - parts_type parts; -}; - - -enum class Compression { - Raw, - Lzma, - Unknown -}; - - -struct FileSource -{ - Compression compression = Compression::Unknown; - Hash hash; - QString url; - std::size_t size = 0; - void upgrade(const FileSource & other) { - if(compression == Compression::Unknown || other.size < size) { - *this = other; - } - } - bool isBad() const { - return compression == Compression::Unknown; - } -}; - -struct File -{ - Hash hash; - bool executable; - std::uint64_t size = 0; -}; - -struct Package { - static Package fromInspectedFolder(const QString &folderPath); - static Package fromManifestFile(const QString &path); - static Package fromManifestContents(const QByteArray& contents); - - explicit operator bool() const - { - return valid; - } - void addFolder(Path folder); - void addFile(const Path & path, const File & file); - void addLink(const Path & path, const Path & target); - void addSource(const FileSource & source); - - std::map sources; - bool valid = true; - std::set folders; - std::map files; - std::map symlinks; -}; - -struct FileDownload : FileSource -{ - FileDownload(const FileSource& source, bool executable) { - static_cast (*this) = source; - this->executable = executable; - } - bool executable = false; -}; - -struct UpdateOperations { - static UpdateOperations resolve(const Package & from, const Package & to); - bool valid = false; - std::vector deletes; - std::vector rmdirs; - std::vector mkdirs; - std::map downloads; - std::map mklinks; - std::map executable_fixes; -}; - -} diff --git a/launcher/net/ByteArraySink.h b/launcher/net/ByteArraySink.h index 501318a11..d6b17d605 100644 --- a/launcher/net/ByteArraySink.h +++ b/launcher/net/ByteArraySink.h @@ -1,7 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-only /* - * PolyMC - Minecraft Launcher + * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln + * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -46,14 +47,17 @@ namespace Net { */ class ByteArraySink : public Sink { public: - ByteArraySink(QByteArray* output) : m_output(output){}; + ByteArraySink(std::shared_ptr output) : m_output(output){}; virtual ~ByteArraySink() = default; public: auto init(QNetworkRequest& request) -> Task::State override { - m_output->clear(); + if (m_output) + m_output->clear(); + else + qWarning() << "ByteArraySink did not initialize the buffer because it's not addressable"; if (initAllValidators(request)) return Task::State::Running; return Task::State::Failed; @@ -61,7 +65,10 @@ class ByteArraySink : public Sink { auto write(QByteArray& data) -> Task::State override { - m_output->append(data); + if (m_output) + m_output->append(data); + else + qWarning() << "ByteArraySink did not write the buffer because it's not addressable"; if (writeAllValidators(data)) return Task::State::Running; return Task::State::Failed; @@ -69,7 +76,10 @@ class ByteArraySink : public Sink { auto abort() -> Task::State override { - m_output->clear(); + if (m_output) + m_output->clear(); + else + qWarning() << "ByteArraySink did not clear the buffer because it's not addressable"; failAllValidators(); return Task::State::Failed; } @@ -84,6 +94,6 @@ class ByteArraySink : public Sink { auto hasLocalData() -> bool override { return false; } private: - QByteArray* m_output; + std::shared_ptr m_output; }; } // namespace Net diff --git a/launcher/net/Download.cpp b/launcher/net/Download.cpp index 7f8d3a067..4ea45c635 100644 --- a/launcher/net/Download.cpp +++ b/launcher/net/Download.cpp @@ -41,6 +41,7 @@ #include #include +#include #include "ByteArraySink.h" #include "ChecksumValidator.h" @@ -69,7 +70,7 @@ auto Download::makeCached(QUrl url, MetaEntryPtr entry, Options options) -> Down return dl; } -auto Download::makeByteArray(QUrl url, QByteArray* output, Options options) -> Download::Ptr +auto Download::makeByteArray(QUrl url, std::shared_ptr output, Options options) -> Download::Ptr { auto dl = makeShared(); dl->m_url = url; diff --git a/launcher/net/Download.h b/launcher/net/Download.h index afa0eb5ce..0f11c52f9 100644 --- a/launcher/net/Download.h +++ b/launcher/net/Download.h @@ -60,7 +60,7 @@ class Download : public NetAction { ~Download() override = default; static auto makeCached(QUrl url, MetaEntryPtr entry, Options options = Option::NoOptions) -> Download::Ptr; - static auto makeByteArray(QUrl url, QByteArray* output, Options options = Option::NoOptions) -> Download::Ptr; + static auto makeByteArray(QUrl url, std::shared_ptr output, Options options = Option::NoOptions) -> Download::Ptr; static auto makeFile(QUrl url, QString path, Options options = Option::NoOptions) -> Download::Ptr; public: diff --git a/launcher/net/Upload.cpp b/launcher/net/Upload.cpp index 4f9553edf..3f6f58290 100644 --- a/launcher/net/Upload.cpp +++ b/launcher/net/Upload.cpp @@ -39,218 +39,226 @@ #include "Upload.h" #include -#include "ByteArraySink.h" -#include "BuildConfig.h" #include "Application.h" +#include "BuildConfig.h" +#include "ByteArraySink.h" #include "net/Logging.h" namespace Net { - bool Upload::abort() - { - if (m_reply) { - m_reply->abort(); - } else { - m_state = State::AbortedByUser; +bool Upload::abort() +{ + if (m_reply) { + m_reply->abort(); + } else { + m_state = State::AbortedByUser; + } + return true; +} + +void Upload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) +{ + setProgress(bytesReceived, bytesTotal); +} + +void Upload::downloadError(QNetworkReply::NetworkError error) +{ + if (error == QNetworkReply::OperationCanceledError) { + qCCritical(taskUploadLogC) << getUid().toString() << "Aborted " << m_url.toString(); + m_state = State::AbortedByUser; + } else { + // error happened during download. + qCCritical(taskUploadLogC) << getUid().toString() << "Failed " << m_url.toString() << " with reason " << error; + m_state = State::Failed; + } +} + +void Upload::sslErrors(const QList& errors) +{ + int i = 1; + for (const auto& error : errors) { + qCCritical(taskUploadLogC) << getUid().toString() << "Upload" << m_url.toString() << "SSL Error #" << i << " : " + << error.errorString(); + auto cert = error.certificate(); + qCCritical(taskUploadLogC) << getUid().toString() << "Certificate in question:\n" << cert.toText(); + i++; + } +} + +bool Upload::handleRedirect() +{ + QUrl redirect = m_reply->header(QNetworkRequest::LocationHeader).toUrl(); + if (!redirect.isValid()) { + if (!m_reply->hasRawHeader("Location")) { + // no redirect -> it's fine to continue + return false; } - return true; - } - - void Upload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) { - setProgress(bytesReceived, bytesTotal); - } - - void Upload::downloadError(QNetworkReply::NetworkError error) { - if (error == QNetworkReply::OperationCanceledError) { - qCCritical(taskUploadLogC) << getUid().toString() << "Aborted " << m_url.toString(); - m_state = State::AbortedByUser; - } else { - // error happened during download. - qCCritical(taskUploadLogC) << getUid().toString() << "Failed " << m_url.toString() << " with reason " << error; - m_state = State::Failed; + // there is a Location header, but it's not correct. we need to apply some workarounds... + QByteArray redirectBA = m_reply->rawHeader("Location"); + if (redirectBA.size() == 0) { + // empty, yet present redirect header? WTF? + return false; } - } - - void Upload::sslErrors(const QList &errors) { - int i = 1; - for (const auto& error : errors) { - qCCritical(taskUploadLogC) << getUid().toString() << "Upload" << m_url.toString() << "SSL Error #" << i << " : " << error.errorString(); - auto cert = error.certificate(); - qCCritical(taskUploadLogC) << getUid().toString() << "Certificate in question:\n" << cert.toText(); - i++; - } - } - - bool Upload::handleRedirect() - { - QUrl redirect = m_reply->header(QNetworkRequest::LocationHeader).toUrl(); - if (!redirect.isValid()) { - if (!m_reply->hasRawHeader("Location")) { - // no redirect -> it's fine to continue - return false; - } - // there is a Location header, but it's not correct. we need to apply some workarounds... - QByteArray redirectBA = m_reply->rawHeader("Location"); - if (redirectBA.size() == 0) { - // empty, yet present redirect header? WTF? - return false; - } - QString redirectStr = QString::fromUtf8(redirectBA); - - if (redirectStr.startsWith("//")) { - /* - * IF the URL begins with //, we need to insert the URL scheme. - * See: https://bugreports.qt.io/browse/QTBUG-41061 - * See: http://tools.ietf.org/html/rfc3986#section-4.2 - */ - redirectStr = m_reply->url().scheme() + ":" + redirectStr; - } else if (redirectStr.startsWith("/")) { - /* - * IF the URL begins with /, we need to process it as a relative URL - */ - auto url = m_reply->url(); - url.setPath(redirectStr, QUrl::TolerantMode); - redirectStr = url.toString(); - } + QString redirectStr = QString::fromUtf8(redirectBA); + if (redirectStr.startsWith("//")) { /* - * Next, make sure the URL is parsed in tolerant mode. Qt doesn't parse the location header in tolerant mode, which causes issues. - * FIXME: report Qt bug for this + * IF the URL begins with //, we need to insert the URL scheme. + * See: https://bugreports.qt.io/browse/QTBUG-41061 + * See: http://tools.ietf.org/html/rfc3986#section-4.2 */ - redirect = QUrl(redirectStr, QUrl::TolerantMode); - if (!redirect.isValid()) { - qCWarning(taskUploadLogC) << getUid().toString() << "Failed to parse redirect URL:" << redirectStr; - downloadError(QNetworkReply::ProtocolFailure); - return false; - } - qCDebug(taskUploadLogC) << getUid().toString() << "Fixed location header:" << redirect; - } else { - qCDebug(taskUploadLogC) << getUid().toString() << "Location header:" << redirect; + redirectStr = m_reply->url().scheme() + ":" + redirectStr; + } else if (redirectStr.startsWith("/")) { + /* + * IF the URL begins with /, we need to process it as a relative URL + */ + auto url = m_reply->url(); + url.setPath(redirectStr, QUrl::TolerantMode); + redirectStr = url.toString(); } - m_url = QUrl(redirect.toString()); - qCDebug(taskUploadLogC) << getUid().toString() << "Following redirect to " << m_url.toString(); - startAction(m_network); - return true; + /* + * Next, make sure the URL is parsed in tolerant mode. Qt doesn't parse the location header in tolerant mode, which causes issues. + * FIXME: report Qt bug for this + */ + redirect = QUrl(redirectStr, QUrl::TolerantMode); + if (!redirect.isValid()) { + qCWarning(taskUploadLogC) << getUid().toString() << "Failed to parse redirect URL:" << redirectStr; + downloadError(QNetworkReply::ProtocolFailure); + return false; + } + qCDebug(taskUploadLogC) << getUid().toString() << "Fixed location header:" << redirect; + } else { + qCDebug(taskUploadLogC) << getUid().toString() << "Location header:" << redirect; } - void Upload::downloadFinished() { - // handle HTTP redirection first - // very unlikely for post requests, still can happen - if (handleRedirect()) { - qCDebug(taskUploadLogC) << getUid().toString() << "Upload redirected:" << m_url.toString(); - return; - } + m_url = QUrl(redirect.toString()); + qCDebug(taskUploadLogC) << getUid().toString() << "Following redirect to " << m_url.toString(); + startAction(m_network); + return true; +} - // if the download failed before this point ... - if (m_state == State::Succeeded) { - qCDebug(taskUploadLogC) << getUid().toString() << "Upload failed but we are allowed to proceed:" << m_url.toString(); - m_sink->abort(); - m_reply.reset(); - emit succeeded(); - return; - } else if (m_state == State::Failed) { - qCDebug(taskUploadLogC) << getUid().toString() << "Upload failed in previous step:" << m_url.toString(); - m_sink->abort(); - m_reply.reset(); - emit failed(""); - return; - } else if (m_state == State::AbortedByUser) { - qCDebug(taskUploadLogC) << getUid().toString() << "Upload aborted in previous step:" << m_url.toString(); - m_sink->abort(); - m_reply.reset(); - emit aborted(); - return; - } +void Upload::downloadFinished() +{ + // handle HTTP redirection first + // very unlikely for post requests, still can happen + if (handleRedirect()) { + qCDebug(taskUploadLogC) << getUid().toString() << "Upload redirected:" << m_url.toString(); + return; + } - // make sure we got all the remaining data, if any - auto data = m_reply->readAll(); - if (data.size()) { - qCDebug(taskUploadLogC) << getUid().toString() << "Writing extra" << data.size() << "bytes"; - m_state = m_sink->write(data); - } - - // otherwise, finalize the whole graph - m_state = m_sink->finalize(*m_reply.get()); - if (m_state != State::Succeeded) { - qCDebug(taskUploadLogC) << getUid().toString() << "Upload failed to finalize:" << m_url.toString(); - m_sink->abort(); - m_reply.reset(); - emit failed(""); - return; - } + // if the download failed before this point ... + if (m_state == State::Succeeded) { + qCDebug(taskUploadLogC) << getUid().toString() << "Upload failed but we are allowed to proceed:" << m_url.toString(); + m_sink->abort(); m_reply.reset(); - qCDebug(taskUploadLogC) << getUid().toString() << "Upload succeeded:" << m_url.toString(); emit succeeded(); + return; + } else if (m_state == State::Failed) { + qCDebug(taskUploadLogC) << getUid().toString() << "Upload failed in previous step:" << m_url.toString(); + m_sink->abort(); + m_reply.reset(); + emit failed(""); + return; + } else if (m_state == State::AbortedByUser) { + qCDebug(taskUploadLogC) << getUid().toString() << "Upload aborted in previous step:" << m_url.toString(); + m_sink->abort(); + m_reply.reset(); + emit aborted(); + return; } - void Upload::downloadReadyRead() { - if (m_state == State::Running) { - auto data = m_reply->readAll(); - m_state = m_sink->write(data); - } + // make sure we got all the remaining data, if any + auto data = m_reply->readAll(); + if (data.size()) { + qCDebug(taskUploadLogC) << getUid().toString() << "Writing extra" << data.size() << "bytes"; + m_state = m_sink->write(data); } - void Upload::executeTask() { - setStatus(tr("Uploading %1").arg(m_url.toString())); + // otherwise, finalize the whole graph + m_state = m_sink->finalize(*m_reply.get()); + if (m_state != State::Succeeded) { + qCDebug(taskUploadLogC) << getUid().toString() << "Upload failed to finalize:" << m_url.toString(); + m_sink->abort(); + m_reply.reset(); + emit failed(""); + return; + } + m_reply.reset(); + qCDebug(taskUploadLogC) << getUid().toString() << "Upload succeeded:" << m_url.toString(); + emit succeeded(); +} - if (m_state == State::AbortedByUser) { - qCWarning(taskUploadLogC) << getUid().toString() << "Attempt to start an aborted Upload:" << m_url.toString(); - emit aborted(); +void Upload::downloadReadyRead() +{ + if (m_state == State::Running) { + auto data = m_reply->readAll(); + m_state = m_sink->write(data); + } +} + +void Upload::executeTask() +{ + setStatus(tr("Uploading %1").arg(m_url.toString())); + + if (m_state == State::AbortedByUser) { + qCWarning(taskUploadLogC) << getUid().toString() << "Attempt to start an aborted Upload:" << m_url.toString(); + emit aborted(); + return; + } + QNetworkRequest request(m_url); + m_state = m_sink->init(request); + switch (m_state) { + case State::Succeeded: + emitSucceeded(); + qCDebug(taskUploadLogC) << getUid().toString() << "Upload cache hit " << m_url.toString(); return; - } - QNetworkRequest request(m_url); - m_state = m_sink->init(request); - switch (m_state) { - case State::Succeeded: - emitSucceeded(); - qCDebug(taskUploadLogC) << getUid().toString() << "Upload cache hit " << m_url.toString(); - return; - case State::Running: - qCDebug(taskUploadLogC) << getUid().toString() << "Uploading " << m_url.toString(); - break; - case State::Inactive: - case State::Failed: - emitFailed(""); - return; - case State::AbortedByUser: - emitAborted(); - return; - } + case State::Running: + qCDebug(taskUploadLogC) << getUid().toString() << "Uploading " << m_url.toString(); + break; + case State::Inactive: + case State::Failed: + emitFailed(""); + return; + case State::AbortedByUser: + emitAborted(); + return; + } - request.setHeader(QNetworkRequest::UserAgentHeader, APPLICATION->getUserAgent().toUtf8()); - // TODO remove duplication - if (APPLICATION->capabilities() & Application::SupportsFlame && request.url().host() == QUrl(BuildConfig.FLAME_BASE_URL).host()) { - request.setRawHeader("x-api-key", APPLICATION->getFlameAPIKey().toUtf8()); - } else if (request.url().host() == QUrl(BuildConfig.MODRINTH_PROD_URL).host() || - request.url().host() == QUrl(BuildConfig.MODRINTH_STAGING_URL).host()) { - QString token = APPLICATION->getModrinthAPIToken(); - if (!token.isNull()) - request.setRawHeader("Authorization", token.toUtf8()); - } + request.setHeader(QNetworkRequest::UserAgentHeader, APPLICATION->getUserAgent().toUtf8()); + // TODO remove duplication + if (APPLICATION->capabilities() & Application::SupportsFlame && request.url().host() == QUrl(BuildConfig.FLAME_BASE_URL).host()) { + request.setRawHeader("x-api-key", APPLICATION->getFlameAPIKey().toUtf8()); + } else if (request.url().host() == QUrl(BuildConfig.MODRINTH_PROD_URL).host() || + request.url().host() == QUrl(BuildConfig.MODRINTH_STAGING_URL).host()) { + QString token = APPLICATION->getModrinthAPIToken(); + if (!token.isNull()) + request.setRawHeader("Authorization", token.toUtf8()); + } - //TODO other types of post requests ? - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - QNetworkReply* rep = m_network->post(request, m_post_data); + // TODO other types of post requests ? + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + QNetworkReply* rep = m_network->post(request, m_post_data); - m_reply.reset(rep); - connect(rep, &QNetworkReply::downloadProgress, this, &Upload::downloadProgress); - connect(rep, &QNetworkReply::finished, this, &Upload::downloadFinished); -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15 - connect(rep, &QNetworkReply::errorOccurred, this, &Upload::downloadError); + m_reply.reset(rep); + connect(rep, &QNetworkReply::downloadProgress, this, &Upload::downloadProgress); + connect(rep, &QNetworkReply::finished, this, &Upload::downloadFinished); +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15 + connect(rep, &QNetworkReply::errorOccurred, this, &Upload::downloadError); #else - connect(rep, QOverload::of(&QNetworkReply::error), this, &Upload::downloadError); + connect(rep, QOverload::of(&QNetworkReply::error), this, &Upload::downloadError); #endif - connect(rep, &QNetworkReply::sslErrors, this, &Upload::sslErrors); - connect(rep, &QNetworkReply::readyRead, this, &Upload::downloadReadyRead); - } + connect(rep, &QNetworkReply::sslErrors, this, &Upload::sslErrors); + connect(rep, &QNetworkReply::readyRead, this, &Upload::downloadReadyRead); +} - Upload::Ptr Upload::makeByteArray(QUrl url, QByteArray *output, QByteArray m_post_data) { - auto up = makeShared(); - up->m_url = std::move(url); - up->m_sink.reset(new ByteArraySink(output)); - up->m_post_data = std::move(m_post_data); - return up; - } -} // Net +Upload::Ptr Upload::makeByteArray(QUrl url, std::shared_ptr output, QByteArray m_post_data) +{ + auto up = makeShared(); + up->m_url = std::move(url); + up->m_sink.reset(new ByteArraySink(output)); + up->m_post_data = std::move(m_post_data); + return up; +} +} // namespace Net diff --git a/launcher/net/Upload.h b/launcher/net/Upload.h index e8f0ea40d..0b0c94976 100644 --- a/launcher/net/Upload.h +++ b/launcher/net/Upload.h @@ -42,31 +42,31 @@ namespace Net { - class Upload : public NetAction { - Q_OBJECT +class Upload : public NetAction { + Q_OBJECT - public: - using Ptr = shared_qobject_ptr; + public: + using Ptr = shared_qobject_ptr; - static Upload::Ptr makeByteArray(QUrl url, QByteArray *output, QByteArray m_post_data); - auto abort() -> bool override; - auto canAbort() const -> bool override { return true; }; + static Upload::Ptr makeByteArray(QUrl url, std::shared_ptr output, QByteArray m_post_data); + auto abort() -> bool override; + auto canAbort() const -> bool override { return true; }; - protected slots: - void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) override; - void downloadError(QNetworkReply::NetworkError error) override; - void sslErrors(const QList & errors) override; - void downloadFinished() override; - void downloadReadyRead() override; + protected slots: + void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) override; + void downloadError(QNetworkReply::NetworkError error) override; + void sslErrors(const QList& errors) override; + void downloadFinished() override; + void downloadReadyRead() override; - public slots: - void executeTask() override; - private: - std::unique_ptr m_sink; - QByteArray m_post_data; + public slots: + void executeTask() override; - bool handleRedirect(); - }; + private: + std::unique_ptr m_sink; + QByteArray m_post_data; -} // Net + bool handleRedirect(); +}; +} // namespace Net diff --git a/launcher/news/NewsChecker.cpp b/launcher/news/NewsChecker.cpp index 1f1520d0a..4f02bf5e0 100644 --- a/launcher/news/NewsChecker.cpp +++ b/launcher/news/NewsChecker.cpp @@ -58,7 +58,7 @@ void NewsChecker::reloadNews() qDebug() << "Reloading news."; NetJob::Ptr job{ new NetJob("News RSS Feed", m_network) }; - job->addNetAction(Net::Download::makeByteArray(m_feedUrl, &newsData)); + job->addNetAction(Net::Download::makeByteArray(m_feedUrl, newsData)); QObject::connect(job.get(), &NetJob::succeeded, this, &NewsChecker::rssDownloadFinished); QObject::connect(job.get(), &NetJob::failed, this, &NewsChecker::rssDownloadFailed); m_newsNetJob.reset(job); @@ -79,32 +79,27 @@ void NewsChecker::rssDownloadFinished() int errorCol = -1; // Parse the XML. - if (!doc.setContent(newsData, false, &errorMsg, &errorLine, &errorCol)) - { + if (!doc.setContent(*newsData, false, &errorMsg, &errorLine, &errorCol)) { QString fullErrorMsg = QString("Error parsing RSS feed XML. %1 at %2:%3.").arg(errorMsg).arg(errorLine).arg(errorCol); fail(fullErrorMsg); - newsData.clear(); + newsData->clear(); return; } - newsData.clear(); + newsData->clear(); } // If the parsing succeeded, read it. QDomNodeList items = doc.elementsByTagName("entry"); m_newsEntries.clear(); - for (int i = 0; i < items.length(); i++) - { + for (int i = 0; i < items.length(); i++) { QDomElement element = items.at(i).toElement(); NewsEntryPtr entry; entry.reset(new NewsEntry()); QString errorMsg = "An unknown error occurred."; - if (NewsEntry::fromXmlElement(element, entry.get(), &errorMsg)) - { + if (NewsEntry::fromXmlElement(element, entry.get(), &errorMsg)) { qDebug() << "Loaded news entry" << entry->title; m_newsEntries.append(entry); - } - else - { + } else { qWarning() << "Failed to load news entry at index" << i << ":" << errorMsg; } } diff --git a/launcher/news/NewsChecker.h b/launcher/news/NewsChecker.h index 8467a5412..41babfff7 100644 --- a/launcher/news/NewsChecker.h +++ b/launcher/news/NewsChecker.h @@ -85,7 +85,7 @@ protected: /* data */ //! True if news has been loaded. bool m_loadedNews; - QByteArray newsData; + std::shared_ptr newsData = std::make_shared(); /*! * Gets the error message that was given last time the news was loaded. diff --git a/launcher/resources/OSX/OSX.qrc b/launcher/resources/OSX/OSX.qrc index 9d4511d1a..49f56b0c1 100644 --- a/launcher/resources/OSX/OSX.qrc +++ b/launcher/resources/OSX/OSX.qrc @@ -16,7 +16,6 @@ scalable/jarmods.svg scalable/java.svg scalable/language.svg - scalable/launcher.svg scalable/loadermods.svg scalable/log.svg scalable/minecraft.svg diff --git a/launcher/resources/OSX/scalable/launcher.svg b/launcher/resources/OSX/scalable/launcher.svg deleted file mode 100644 index aeee84338..000000000 --- a/launcher/resources/OSX/scalable/launcher.svg +++ /dev/null @@ -1,57 +0,0 @@ - - - - Prism Launcher Logo - - - - - - - - - - - - - - - - - - - - - - - Prism Launcher Logo - 19/10/2022 - - - Prism Launcher - - - - - AutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zeke - - - https://github.com/PrismLauncher/PrismLauncher - - - Prism Launcher - - - - - - - - - - - - - - - diff --git a/launcher/resources/breeze_dark/scalable/launcher.svg b/launcher/resources/breeze_dark/scalable/launcher.svg deleted file mode 100644 index aeee84338..000000000 --- a/launcher/resources/breeze_dark/scalable/launcher.svg +++ /dev/null @@ -1,57 +0,0 @@ - - - - Prism Launcher Logo - - - - - - - - - - - - - - - - - - - - - - - Prism Launcher Logo - 19/10/2022 - - - Prism Launcher - - - - - AutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zeke - - - https://github.com/PrismLauncher/PrismLauncher - - - Prism Launcher - - - - - - - - - - - - - - - diff --git a/launcher/resources/flat/flat.qrc b/launcher/resources/flat/flat.qrc index cadf87364..2fd5daefe 100644 --- a/launcher/resources/flat/flat.qrc +++ b/launcher/resources/flat/flat.qrc @@ -18,7 +18,6 @@ scalable/jarmods.svg scalable/java.svg scalable/language.svg - scalable/launcher.svg scalable/loadermods.svg scalable/log.svg scalable/minecraft.svg diff --git a/launcher/resources/flat/scalable/launcher.svg b/launcher/resources/flat/scalable/launcher.svg deleted file mode 100644 index aeee84338..000000000 --- a/launcher/resources/flat/scalable/launcher.svg +++ /dev/null @@ -1,57 +0,0 @@ - - - - Prism Launcher Logo - - - - - - - - - - - - - - - - - - - - - - - Prism Launcher Logo - 19/10/2022 - - - Prism Launcher - - - - - AutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zeke - - - https://github.com/PrismLauncher/PrismLauncher - - - Prism Launcher - - - - - - - - - - - - - - - diff --git a/launcher/resources/flat_white/flat_white.qrc b/launcher/resources/flat_white/flat_white.qrc index 2701462fe..a1c940da0 100644 --- a/launcher/resources/flat_white/flat_white.qrc +++ b/launcher/resources/flat_white/flat_white.qrc @@ -18,7 +18,6 @@ scalable/jarmods.svg scalable/java.svg scalable/language.svg - scalable/launcher.svg scalable/loadermods.svg scalable/log.svg scalable/minecraft.svg diff --git a/launcher/resources/flat_white/scalable/launcher.svg b/launcher/resources/flat_white/scalable/launcher.svg deleted file mode 100644 index 54131b657..000000000 --- a/launcher/resources/flat_white/scalable/launcher.svg +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/launcher/resources/iOS/iOS.qrc b/launcher/resources/iOS/iOS.qrc index 0b79efb2a..9b8d84f50 100644 --- a/launcher/resources/iOS/iOS.qrc +++ b/launcher/resources/iOS/iOS.qrc @@ -16,7 +16,6 @@ scalable/jarmods.svg scalable/java.svg scalable/language.svg - scalable/launcher.svg scalable/loadermods.svg scalable/log.svg scalable/minecraft.svg diff --git a/launcher/resources/iOS/scalable/launcher.svg b/launcher/resources/iOS/scalable/launcher.svg deleted file mode 100644 index aeee84338..000000000 --- a/launcher/resources/iOS/scalable/launcher.svg +++ /dev/null @@ -1,57 +0,0 @@ - - - - Prism Launcher Logo - - - - - - - - - - - - - - - - - - - - - - - Prism Launcher Logo - 19/10/2022 - - - Prism Launcher - - - - - AutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zeke - - - https://github.com/PrismLauncher/PrismLauncher - - - Prism Launcher - - - - - - - - - - - - - - - diff --git a/launcher/resources/pe_blue/pe_blue.qrc b/launcher/resources/pe_blue/pe_blue.qrc index 1b2b62915..da45ef9a1 100644 --- a/launcher/resources/pe_blue/pe_blue.qrc +++ b/launcher/resources/pe_blue/pe_blue.qrc @@ -16,7 +16,6 @@ scalable/jarmods.svg scalable/java.svg scalable/language.svg - scalable/launcher.svg scalable/loadermods.svg scalable/log.svg scalable/minecraft.svg diff --git a/launcher/resources/pe_blue/scalable/launcher.svg b/launcher/resources/pe_blue/scalable/launcher.svg deleted file mode 100644 index aeee84338..000000000 --- a/launcher/resources/pe_blue/scalable/launcher.svg +++ /dev/null @@ -1,57 +0,0 @@ - - - - Prism Launcher Logo - - - - - - - - - - - - - - - - - - - - - - - Prism Launcher Logo - 19/10/2022 - - - Prism Launcher - - - - - AutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zeke - - - https://github.com/PrismLauncher/PrismLauncher - - - Prism Launcher - - - - - - - - - - - - - - - diff --git a/launcher/resources/pe_colored/pe_colored.qrc b/launcher/resources/pe_colored/pe_colored.qrc index 084fca930..ba5bd44f9 100644 --- a/launcher/resources/pe_colored/pe_colored.qrc +++ b/launcher/resources/pe_colored/pe_colored.qrc @@ -16,7 +16,6 @@ scalable/jarmods.svg scalable/java.svg scalable/language.svg - scalable/launcher.svg scalable/loadermods.svg scalable/log.svg scalable/minecraft.svg diff --git a/launcher/resources/pe_colored/scalable/launcher.svg b/launcher/resources/pe_colored/scalable/launcher.svg deleted file mode 100644 index aeee84338..000000000 --- a/launcher/resources/pe_colored/scalable/launcher.svg +++ /dev/null @@ -1,57 +0,0 @@ - - - - Prism Launcher Logo - - - - - - - - - - - - - - - - - - - - - - - Prism Launcher Logo - 19/10/2022 - - - Prism Launcher - - - - - AutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zeke - - - https://github.com/PrismLauncher/PrismLauncher - - - Prism Launcher - - - - - - - - - - - - - - - diff --git a/launcher/resources/pe_dark/pe_dark.qrc b/launcher/resources/pe_dark/pe_dark.qrc index 5c49b75a3..2bfec42cb 100644 --- a/launcher/resources/pe_dark/pe_dark.qrc +++ b/launcher/resources/pe_dark/pe_dark.qrc @@ -16,7 +16,6 @@ scalable/jarmods.svg scalable/java.svg scalable/language.svg - scalable/launcher.svg scalable/loadermods.svg scalable/log.svg scalable/minecraft.svg diff --git a/launcher/resources/pe_dark/scalable/launcher.svg b/launcher/resources/pe_dark/scalable/launcher.svg deleted file mode 100644 index aeee84338..000000000 --- a/launcher/resources/pe_dark/scalable/launcher.svg +++ /dev/null @@ -1,57 +0,0 @@ - - - - Prism Launcher Logo - - - - - - - - - - - - - - - - - - - - - - - Prism Launcher Logo - 19/10/2022 - - - Prism Launcher - - - - - AutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zeke - - - https://github.com/PrismLauncher/PrismLauncher - - - Prism Launcher - - - - - - - - - - - - - - - diff --git a/launcher/resources/pe_light/pe_light.qrc b/launcher/resources/pe_light/pe_light.qrc index a8e3f1572..25d5da73b 100644 --- a/launcher/resources/pe_light/pe_light.qrc +++ b/launcher/resources/pe_light/pe_light.qrc @@ -16,7 +16,6 @@ scalable/jarmods.svg scalable/java.svg scalable/language.svg - scalable/launcher.svg scalable/loadermods.svg scalable/log.svg scalable/minecraft.svg diff --git a/launcher/resources/pe_light/scalable/launcher.svg b/launcher/resources/pe_light/scalable/launcher.svg deleted file mode 100644 index aeee84338..000000000 --- a/launcher/resources/pe_light/scalable/launcher.svg +++ /dev/null @@ -1,57 +0,0 @@ - - - - Prism Launcher Logo - - - - - - - - - - - - - - - - - - - - - - - Prism Launcher Logo - 19/10/2022 - - - Prism Launcher - - - - - AutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zeke - - - https://github.com/PrismLauncher/PrismLauncher - - - Prism Launcher - - - - - - - - - - - - - - - diff --git a/launcher/settings/INIFile.cpp b/launcher/settings/INIFile.cpp index f0347cabf..d16256b96 100644 --- a/launcher/settings/INIFile.cpp +++ b/launcher/settings/INIFile.cpp @@ -45,12 +45,12 @@ #include -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 5ee145055..9aada5e69 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(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 8a0043152..6054b4b6a 100644 --- a/launcher/translations/TranslationsModel.cpp +++ b/launcher/translations/TranslationsModel.cpp @@ -190,7 +190,7 @@ struct TranslationsModel::Private std::unique_ptr m_qt_translator; std::unique_ptr 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; @@ -671,8 +671,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 72b7db641..496738e32 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -2,7 +2,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu - * Copyright (C) 2022 TheKodeToad + * Copyright (C) 2023 TheKodeToad * * 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(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(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 @@ -219,6 +224,13 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi // disabled until we have an instance selected ui->instanceToolBar->setEnabled(false); setInstanceActionsEnabled(false); + + // add a close button at the end of the main toolbar when running on gamescope / steam deck + // FIXME: detect if we don't have server side decorations instead + if (qgetenv("XDG_CURRENT_DESKTOP") == "gamescope") { + ui->mainToolBar->addAction(ui->actionCloseWindow); + } + } // add the toolbar toggles to the view menu @@ -414,15 +426,6 @@ void MainWindow::keyReleaseEvent(QKeyEvent *event) void MainWindow::retranslateUi() { - auto accounts = APPLICATION->accounts(); - MinecraftAccountPtr defaultAccount = accounts->defaultAccount(); - if(defaultAccount) { - auto profileLabel = profileInUseFilter(defaultAccount->profileName(), defaultAccount->isInUse()); - ui->actionAccountsButton->setText(profileLabel); - } - else { - ui->actionAccountsButton->setText(tr("Accounts")); - } if (m_selectedInstance) { m_statusLeft->setText(m_selectedInstance->getStatusbarDescription()); @@ -432,6 +435,12 @@ void MainWindow::retranslateUi() ui->retranslateUi(this); + MinecraftAccountPtr defaultAccount = APPLICATION->accounts()->defaultAccount(); + if(defaultAccount) { + auto profileLabel = profileInUseFilter(defaultAccount->profileName(), defaultAccount->isInUse()); + ui->actionAccountsButton->setText(profileLabel); + } + changeIconButton->setToolTip(ui->actionChangeInstIcon->toolTip()); renameButton->setToolTip(ui->actionRenameInstance->toolTip()); @@ -471,7 +480,23 @@ void MainWindow::lockToolbars(bool state) void MainWindow::konamiTriggered() { - qDebug() << "Super Secret Mode ACTIVATED!"; + QString gradient = " stop:0 rgba(125, 0, 0, 255), stop:0.166 rgba(125, 125, 0, 255), stop:0.333 rgba(0, 125, 0, 255), stop:0.5 rgba(0, 125, 125, 255), stop:0.666 rgba(0, 0, 125, 255), stop:0.833 rgba(125, 0, 125, 255), stop:1 rgba(125, 0, 0, 255));"; + QString stylesheet = "background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:0," + gradient; + if (ui->mainToolBar->styleSheet() == stylesheet) { + ui->mainToolBar->setStyleSheet(""); + ui->instanceToolBar->setStyleSheet(""); + ui->centralWidget->setStyleSheet(""); + ui->newsToolBar->setStyleSheet(""); + ui->statusBar->setStyleSheet(""); + qDebug() << "Super Secret Mode DEACTIVATED!"; + } else { + ui->mainToolBar->setStyleSheet(stylesheet); + ui->instanceToolBar->setStyleSheet("background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:0, y2:1," + gradient); + ui->centralWidget->setStyleSheet("background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:1," + gradient); + ui->newsToolBar->setStyleSheet(stylesheet); + ui->statusBar->setStyleSheet(stylesheet); + qDebug() << "Super Secret Mode ACTIVATED!"; + } } void MainWindow::showInstanceContextMenu(const QPoint &pos) @@ -673,6 +698,15 @@ void MainWindow::repopulateAccountsMenu() { ui->accountsMenu->clear(); + // NOTE: this is done so the accounts button text is not set to the accounts menu title + QMenu *accountsButtonMenu = ui->actionAccountsButton->menu(); + if (accountsButtonMenu) { + accountsButtonMenu->clear(); + } else { + accountsButtonMenu = new QMenu(this); + ui->actionAccountsButton->setMenu(accountsButtonMenu); + } + auto accounts = APPLICATION->accounts(); MinecraftAccountPtr defaultAccount = accounts->defaultAccount(); @@ -687,6 +721,8 @@ void MainWindow::repopulateAccountsMenu() } } + QActionGroup* accountsGroup = new QActionGroup(this); + if (accounts->count() <= 0) { ui->actionNoAccountsAdded->setEnabled(false); @@ -702,6 +738,7 @@ void MainWindow::repopulateAccountsMenu() QAction *action = new QAction(profileLabel, this); action->setData(i); action->setCheckable(true); + action->setActionGroup(accountsGroup); if (defaultAccount == account) { action->setChecked(true); @@ -730,6 +767,7 @@ void MainWindow::repopulateAccountsMenu() ui->actionNoDefaultAccount->setData(-1); ui->actionNoDefaultAccount->setChecked(!defaultAccount); + ui->actionNoDefaultAccount->setActionGroup(accountsGroup); ui->accountsMenu->addAction(ui->actionNoDefaultAccount); @@ -737,6 +775,8 @@ void MainWindow::repopulateAccountsMenu() ui->accountsMenu->addSeparator(); ui->accountsMenu->addAction(ui->actionManageAccounts); + + accountsButtonMenu->addActions(ui->accountsMenu->actions()); } void MainWindow::updatesAllowedChanged(bool allowed) @@ -1201,6 +1241,12 @@ void MainWindow::on_actionViewInstanceFolder_triggered() DesktopServices::openDirectory(str); } +void MainWindow::on_actionViewLauncherRootFolder_triggered() +{ + const QString dataPath = QDir::currentPath(); + DesktopServices::openDirectory(dataPath); +} + void MainWindow::refreshInstances() { APPLICATION->instances()->loadList(); @@ -1359,7 +1405,7 @@ void MainWindow::on_actionDeleteInstance_triggered() APPLICATION->instances()->deleteInstance(id); } -void MainWindow::on_actionExportInstance_triggered() +void MainWindow::on_actionExportInstanceZip_triggered() { if (m_selectedInstance) { @@ -1368,6 +1414,15 @@ void MainWindow::on_actionExportInstance_triggered() } } +void MainWindow::on_actionExportInstanceMrPack_triggered() +{ + if (m_selectedInstance) + { + ExportMrPackDialog dlg(m_selectedInstance, this); + dlg.exec(); + } +} + void MainWindow::on_actionRenameInstance_triggered() { if (m_selectedInstance) @@ -1455,140 +1510,113 @@ void MainWindow::on_actionKillInstance_triggered() void MainWindow::on_actionCreateInstanceShortcut_triggered() { - if (m_selectedInstance) - { - auto desktopPath = FS::getDesktopDir(); - if (desktopPath.isEmpty()) { - // TODO come up with an alternative solution (open "save file" dialog) - QMessageBox::critical(this, tr("Create instance shortcut"), tr("Couldn't find desktop?!")); - return; - } + if (!m_selectedInstance) + return; + auto desktopPath = FS::getDesktopDir(); + if (desktopPath.isEmpty()) { + // TODO come up with an alternative solution (open "save file" dialog) + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Couldn't find desktop?!")); + return; + } + QString desktopFilePath; + QString appPath = QApplication::applicationFilePath(); + QString iconPath; + QStringList args; #if defined(Q_OS_MACOS) - QString appPath = QApplication::applicationFilePath(); - if (appPath.startsWith("/private/var/")) { - QMessageBox::critical(this, tr("Create instance shortcut"), tr("The launcher is in the folder it was extracted from, therefore it cannot create shortcuts.")); - return; - } - - if (FS::createShortcut(FS::PathCombine(desktopPath, m_selectedInstance->name()), - appPath, { "--launch", m_selectedInstance->id() }, - m_selectedInstance->name(), "")) { - QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance on your desktop!")); - } - else - { - QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create instance shortcut!")); - } + if (appPath.startsWith("/private/var/")) { + QMessageBox::critical(this, tr("Create instance shortcut"), + tr("The launcher is in the folder it was extracted from, therefore it cannot create shortcuts.")); + return; + } #elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) - QString appPath = QApplication::applicationFilePath(); - if (appPath.startsWith("/tmp/.mount_")) { - // AppImage! - appPath = QProcessEnvironment::systemEnvironment().value(QStringLiteral("APPIMAGE")); - if (appPath.isEmpty()) - { - QMessageBox::critical(this, tr("Create instance shortcut"), tr("Launcher is running as misconfigured AppImage? ($APPIMAGE environment variable is missing)")); - } - else if (appPath.endsWith("/")) - { - appPath.chop(1); - } + if (appPath.startsWith("/tmp/.mount_")) { + // AppImage! + appPath = QProcessEnvironment::systemEnvironment().value(QStringLiteral("APPIMAGE")); + if (appPath.isEmpty()) { + QMessageBox::critical(this, tr("Create instance shortcut"), + tr("Launcher is running as misconfigured AppImage? ($APPIMAGE environment variable is missing)")); + } else if (appPath.endsWith("/")) { + appPath.chop(1); } + } - auto icon = APPLICATION->icons()->icon(m_selectedInstance->iconKey()); - if (icon == nullptr) - { - icon = APPLICATION->icons()->icon("grass"); - } + auto icon = APPLICATION->icons()->icon(m_selectedInstance->iconKey()); + if (icon == nullptr) { + icon = APPLICATION->icons()->icon("grass"); + } - QString iconPath = FS::PathCombine(m_selectedInstance->instanceRoot(), "icon.png"); + iconPath = FS::PathCombine(m_selectedInstance->instanceRoot(), "icon.png"); - QFile iconFile(iconPath); - if (!iconFile.open(QFile::WriteOnly)) - { - QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut.")); - return; - } - bool success = icon->icon().pixmap(64, 64).save(&iconFile, "PNG"); - iconFile.close(); + QFile iconFile(iconPath); + if (!iconFile.open(QFile::WriteOnly)) { + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut.")); + return; + } + bool success = icon->icon().pixmap(64, 64).save(&iconFile, "PNG"); + iconFile.close(); - if (!success) - { - iconFile.remove(); - QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut.")); - return; - } + if (!success) { + iconFile.remove(); + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut.")); + return; + } + + if (DesktopServices::isFlatpak()) { + desktopFilePath = FS::PathCombine(desktopPath, FS::RemoveInvalidFilenameChars(m_selectedInstance->name()) + ".desktop"); + QFileDialog fileDialog; + // workaround to make sure the portal file dialog opens in the desktop directory + fileDialog.setDirectoryUrl(desktopPath); + desktopFilePath = fileDialog.getSaveFileName(this, tr("Create Shortcut"), desktopFilePath, tr("Desktop Entries (*.desktop)")); + if (desktopFilePath.isEmpty()) + return; // file dialog canceled by user + appPath = "flatpak"; + QString flatpakAppId = BuildConfig.LAUNCHER_DESKTOPFILENAME; + flatpakAppId.remove(".desktop"); + args.append({ "run", flatpakAppId }); + } - QString desktopFilePath = FS::PathCombine(desktopPath, m_selectedInstance->name() + ".desktop"); - QStringList args; - if (DesktopServices::isFlatpak()) { - QFileDialog fileDialog; - // workaround to make sure the portal file dialog opens in the desktop directory - fileDialog.setDirectoryUrl(desktopPath); - desktopFilePath = fileDialog.getSaveFileName( - this, tr("Create Shortcut"), desktopFilePath, - tr("Desktop Entries (*.desktop)")); - if (desktopFilePath.isEmpty()) - return; // file dialog canceled by user - appPath = "flatpak"; - QString flatpakAppId = BuildConfig.LAUNCHER_DESKTOPFILENAME; - flatpakAppId.remove(".desktop"); - args.append({ "run", flatpakAppId }); - } - args.append({ "--launch", m_selectedInstance->id() }); - if (FS::createShortcut(desktopFilePath, appPath, args, m_selectedInstance->name(), iconPath)) { - QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance on your desktop!")); - } - else - { - iconFile.remove(); - QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create instance shortcut!")); - } #elif defined(Q_OS_WIN) - auto icon = APPLICATION->icons()->icon(m_selectedInstance->iconKey()); - if (icon == nullptr) - { - icon = APPLICATION->icons()->icon("grass"); - } + auto icon = APPLICATION->icons()->icon(m_selectedInstance->iconKey()); + if (icon == nullptr) { + icon = APPLICATION->icons()->icon("grass"); + } - QString iconPath = FS::PathCombine(m_selectedInstance->instanceRoot(), "icon.ico"); + iconPath = FS::PathCombine(m_selectedInstance->instanceRoot(), "icon.ico"); - // part of fix for weird bug involving the window icon being replaced - // dunno why it happens, but this 2-line fix seems to be enough, so w/e - auto appIcon = APPLICATION->getThemedIcon("logo"); + // part of fix for weird bug involving the window icon being replaced + // dunno why it happens, but this 2-line fix seems to be enough, so w/e + auto appIcon = APPLICATION->getThemedIcon("logo"); - QFile iconFile(iconPath); - if (!iconFile.open(QFile::WriteOnly)) - { - QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut.")); - return; - } - bool success = icon->icon().pixmap(64, 64).save(&iconFile, "ICO"); - iconFile.close(); + QFile iconFile(iconPath); + if (!iconFile.open(QFile::WriteOnly)) { + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut.")); + return; + } + bool success = icon->icon().pixmap(64, 64).save(&iconFile, "ICO"); + iconFile.close(); - // restore original window icon - QGuiApplication::setWindowIcon(appIcon); + // restore original window icon + QGuiApplication::setWindowIcon(appIcon); - if (!success) - { - iconFile.remove(); - QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut.")); - return; - } + if (!success) { + iconFile.remove(); + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut.")); + return; + } - if (FS::createShortcut(FS::PathCombine(desktopPath, m_selectedInstance->name()), - QApplication::applicationFilePath(), { "--launch", m_selectedInstance->id() }, - m_selectedInstance->name(), iconPath)) { - QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance on your desktop!")); - } - else - { - iconFile.remove(); - QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create instance shortcut!")); - } #else - QMessageBox::critical(this, tr("Create instance shortcut"), tr("Not supported on your platform!")); + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Not supported on your platform!")); + return; #endif + args.append({ "--launch", m_selectedInstance->id() }); + if (FS::createShortcut(desktopFilePath, appPath, args, m_selectedInstance->name(), iconPath)) { + QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance on your desktop!")); + } else { +#if not defined(Q_OS_MACOS) + iconFile.remove(); +#endif + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create instance shortcut!")); } } diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h index 3a42c34e1..3bb20c4a4 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 + * Copyright (C) 2023 TheKodeToad * * 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 2b6a10b10..113dfc1e0 100644 --- a/launcher/ui/MainWindow.ui +++ b/launcher/ui/MainWindow.ui @@ -187,6 +187,7 @@ true + @@ -459,10 +460,23 @@ E&xport... - Export the selected instance as a zip file. + Export the selected instance to supported formats. - - Ctrl+E + + + + + + + Prism Launcher (zip) + + + + + + + + Modrinth (mrpack) @@ -528,6 +542,18 @@ Open the instance folder in a file browser. + + + + .. + + + &View Launcher Root Folder + + + Open the launcher's root folder in a file browser. + + @@ -551,7 +577,7 @@ .. - Report a &Bug... + Report a Bug or Suggest a Feature Open the bug tracker to report a bug with %1. diff --git a/launcher/ui/dialogs/AboutDialog.cpp b/launcher/ui/dialogs/AboutDialog.cpp index 76e3d8ed0..88739463f 100644 --- a/launcher/ui/dialogs/AboutDialog.cpp +++ b/launcher/ui/dialogs/AboutDialog.cpp @@ -71,13 +71,18 @@ QString getCreditsHtml() //: %1 is the name of the launcher, determined at build time, e.g. "Prism Launcher Developers" stream << "

" << QObject::tr("%1 Developers", "About Credits").arg(BuildConfig.LAUNCHER_DISPLAYNAME) << "

\n"; stream << QString("

Sefa Eyeoglu (Scrumplex) %1

\n") .arg(getWebsite("https://scrumplex.net")); - stream << QString("

dada513 %1

\n") .arg(getGitHub("dada513")); + stream << QString("

d-513 %1

\n") .arg(getGitHub("d-513")); stream << QString("

txtsd %1

\n") .arg(getWebsite("https://ihavea.quest")); stream << QString("

timoreo %1

\n") .arg(getGitHub("timoreo22")); stream << QString("

Ezekiel Smith (ZekeSmith) %1

\n") .arg(getGitHub("ZekeSmith")); stream << QString("

cozyGalvinism %1

\n") .arg(getGitHub("cozyGalvinism")); - stream << QString("

DioEgizio %1

\n") .arg(getGitHub("DioEgizio")); - stream << QString("

flowln %1

\n") .arg(getGitHub("flowln")); + stream << QString("

DioEgizio %1

\n") .arg(getGitHub("DioEgizio")); + stream << QString("

flowln %1

\n") .arg(getGitHub("flowln")); + stream << QString("

ViRb3 %1

\n") .arg(getGitHub("ViRb3")); + stream << QString("

Rachel Powers (Ryex) %1

\n") .arg(getGitHub("Ryex")); + stream << QString("

TayouVR %1

\n") .arg(getGitHub("TayouVR")); + stream << QString("

TheKodeToad %1

\n") .arg(getGitHub("TheKodeToad")); + stream << QString("

getchoo %1

\n") .arg(getGitHub("getchoo")); stream << "
\n"; // TODO: possibly retrieve from git history at build time? diff --git a/launcher/ui/dialogs/ExportInstanceDialog.cpp b/launcher/ui/dialogs/ExportInstanceDialog.cpp index 07ec3c709..cc41c394d 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 + * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -34,313 +35,39 @@ */ #include "ExportInstanceDialog.h" -#include "ui_ExportInstanceDialog.h" #include #include #include -#include #include +#include +#include "FileIgnoreProxy.h" +#include "ui_ExportInstanceDialog.h" -#include -#include -#include -#include -#include - -#include "StringUtils.h" -#include "SeparatorPrefixTree.h" -#include "Application.h" -#include #include +#include +#include +#include +#include +#include +#include +#include +#include "Application.h" +#include "SeparatorPrefixTree.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(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(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(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(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 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 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(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) +ExportInstanceDialog::ExportInstanceDialog(InstancePtr instance, QWidget* parent) : QDialog(parent), ui(new Ui::ExportInstanceDialog), m_instance(instance) { ui->setupUi(this); auto model = new QFileSystemModel(this); - proxyModel = new PackIgnoreProxy(m_instance, this); - loadPackIgnore(); - proxyModel->setSourceModel(model); + model->setIconProvider(&icons); auto root = instance->instanceRoot(); + proxyModel = new FileIgnoreProxy(root, this); + proxyModel->setSourceModel(model); + auto prefix = QDir(instance->instanceRoot()).relativeFilePath(instance->gameRoot()); + proxyModel->ignoreFilesWithPath().insert({ FS::PathCombine(prefix, "logs"), FS::PathCombine(prefix, "crash-reports") }); + proxyModel->ignoreFilesWithName().append({ ".DS_Store", "thumbs.db", "Thumbs.db" }); + loadPackIgnore(); + ui->treeView->setModel(proxyModel); ui->treeView->setRootIndex(proxyModel->mapFromSource(model->index(root))); ui->treeView->sortByColumn(0, Qt::AscendingOrder); @@ -404,30 +131,17 @@ bool ExportInstanceDialog::doExport() const QString output = QFileDialog::getSaveFileName( this, tr("Export %1").arg(m_instance->name()), - FS::PathCombine(QDir::homePath(), name + ".zip"), "Zip (*.zip)", nullptr, QFileDialog::DontConfirmOverwrite); + FS::PathCombine(QDir::homePath(), name + ".zip"), "Zip (*.zip)", nullptr); if (output.isEmpty()) { return false; } - if (QFile::exists(output)) - { - int ret = - QMessageBox::question(this, tr("Overwrite?"), - tr("This file already exists. Do you want to overwrite it?"), - QMessageBox::No, QMessageBox::Yes); - if (ret == QMessageBox::No) - { - return false; - } - } SaveIcon(m_instance); - auto & blocked = proxyModel->blockedPaths(); - using std::placeholders::_1; auto files = QFileInfoList(); if (!MMCZip::collectFileListRecursively(m_instance->instanceRoot(), nullptr, &files, - std::bind(&SeparatorPrefixTree<'/'>::covers, blocked, _1))) { + std::bind(&FileIgnoreProxy::filterFile, proxyModel, std::placeholders::_1))) { QMessageBox::warning(this, tr("Error"), tr("Unable to export instance")); return false; } @@ -511,5 +225,3 @@ void ExportInstanceDialog::savePackIgnore() qWarning() << e.cause(); } } - -#include "ExportInstanceDialog.moc" diff --git a/launcher/ui/dialogs/ExportInstanceDialog.h b/launcher/ui/dialogs/ExportInstanceDialog.h index dea02d1be..5e8018751 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 * - * 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 . + * + * 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 #include #include +#include "FileIgnoreProxy.h" +#include "FastFileIconProvider.h" class BaseInstance; -class PackIgnoreProxy; typedef std::shared_ptr 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 000000000..b302f7ba5 --- /dev/null +++ b/launcher/ui/dialogs/ExportMrPackDialog.cpp @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 TheKodeToad + * + * 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 . + */ + +#include "ExportMrPackDialog.h" +#include "minecraft/mod/ModFolderModel.h" +#include "ui/dialogs/CustomMessageBox.h" +#include "ui/dialogs/ProgressDialog.h" +#include "ui_ExportMrPackDialog.h" + +#include +#include +#include +#include +#include +#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->ignoreFilesWithPath().insert({ "logs", "crash-reports" }); + proxy->ignoreFilesWithName().append({ ".DS_Store", "thumbs.db", "Thumbs.db" }); + proxy->setSourceModel(model); + + const QDir::Filters filter(QDir::AllEntries | QDir::NoDotAndDotDot | QDir::AllDirs | QDir::Hidden); + + for (const QString& file : root.entryList(filter)) { + if (!(file == "mods" || file == "coremods" || file == "datapacks" || file == "config" || file == "options.txt" || + file == "servers.dat")) + proxy->blockedPaths().insert(file); + } + + MinecraftInstance* mcInstance = dynamic_cast(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, + std::bind(&FileIgnoreProxy::filterFile, proxy, std::placeholders::_1)); + + connect(&task, &Task::failed, + [this](const QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); }); + connect(&task, &Task::aborted, [this] { + CustomMessageBox::selectable(this, tr("Task aborted"), tr("The task has been aborted by the user."), QMessageBox::Information) + ->show(); + }); + + 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 000000000..1c70c4ae1 --- /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 + * + * 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 . + */ + +#pragma once + +#include +#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 000000000..9a7897378 --- /dev/null +++ b/launcher/ui/dialogs/ExportMrPackDialog.ui @@ -0,0 +1,136 @@ + + + ExportMrPackDialog + + + + 0 + 0 + 650 + 413 + + + + Export Modrinth Pack + + + true + + + + + + Information + + + + + + Summary + + + + + + + + + + Name + + + + + + + Version + + + + + + + + + + 1.0.0 + + + + + + + + + + Files + + + + + + + true + + + QAbstractItemView::ExtendedSelection + + + true + + + false + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + name + version + summary + treeView + + + + + buttonBox + accepted() + ExportMrPackDialog + accept() + + + 324 + 390 + + + 324 + 206 + + + + + buttonBox + rejected() + ExportMrPackDialog + reject() + + + 324 + 390 + + + 324 + 206 + + + + + diff --git a/launcher/ui/dialogs/NewInstanceDialog.cpp b/launcher/ui/dialogs/NewInstanceDialog.cpp index aafaf2202..7b9bb944c 100644 --- a/launcher/ui/dialogs/NewInstanceDialog.cpp +++ b/launcher/ui/dialogs/NewInstanceDialog.cpp @@ -54,7 +54,7 @@ #include #include "ui/widgets/PageContainer.h" -#include "ui/pages/modplatform/VanillaPage.h" +#include "ui/pages/modplatform/CustomPage.h" #include "ui/pages/modplatform/atlauncher/AtlPage.h" #include "ui/pages/modplatform/legacy_ftb/Page.h" #include "ui/pages/modplatform/flame/FlamePage.h" @@ -162,7 +162,7 @@ QList NewInstanceDialog::getPages() importPage = new ImportPage(this); - pages.append(new VanillaPage(this)); + pages.append(new CustomPage(this)); pages.append(importPage); pages.append(new AtlPage(this)); if (APPLICATION->capabilities() & Application::SupportsFlame) diff --git a/launcher/ui/dialogs/NewsDialog.cpp b/launcher/ui/dialogs/NewsDialog.cpp index e1b5dd740..b646e3918 100644 --- a/launcher/ui/dialogs/NewsDialog.cpp +++ b/launcher/ui/dialogs/NewsDialog.cpp @@ -32,7 +32,7 @@ NewsDialog::~NewsDialog() void NewsDialog::selectedArticleChanged(const QString& new_title) { - auto const& article_entry = m_entries.constFind(new_title).value(); + auto article_entry = m_entries.constFind(new_title).value(); ui->articleTitleLabel->setText(QString("%2").arg(article_entry->link, new_title)); diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.cpp b/launcher/ui/dialogs/ResourceDownloadDialog.cpp index d2a8d33eb..4f59f5605 100644 --- a/launcher/ui/dialogs/ResourceDownloadDialog.cpp +++ b/launcher/ui/dialogs/ResourceDownloadDialog.cpp @@ -18,17 +18,24 @@ */ #include "ResourceDownloadDialog.h" +#include +#include #include +#include #include "Application.h" #include "ResourceDownloadTask.h" #include "minecraft/mod/ModFolderModel.h" #include "minecraft/mod/ResourcePackFolderModel.h" -#include "minecraft/mod/TexturePackFolderModel.h" #include "minecraft/mod/ShaderPackFolderModel.h" +#include "minecraft/mod/TexturePackFolderModel.h" +#include "minecraft/mod/tasks/GetModDependenciesTask.h" +#include "modplatform/ModIndex.h" +#include "ui/dialogs/CustomMessageBox.h" +#include "ui/dialogs/ProgressDialog.h" #include "ui/dialogs/ReviewMessageBox.h" #include "ui/pages/modplatform/ResourcePage.h" @@ -41,7 +48,10 @@ namespace ResourceDownload { ResourceDownloadDialog::ResourceDownloadDialog(QWidget* parent, const std::shared_ptr 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 +112,8 @@ void ResourceDownloadDialog::initializeContainer() void ResourceDownloadDialog::connectButtons() { auto OkButton = m_buttons.button(QDialogButtonBox::Ok); - OkButton->setToolTip(tr("Opens a new popup to review your selected %1 and confirm your selection. Shortcut: Ctrl+Return").arg(resourcesString())); + OkButton->setToolTip( + tr("Opens a new popup to review your selected %1 and confirm your selection. Shortcut: Ctrl+Return").arg(resourcesString())); connect(OkButton, &QPushButton::clicked, this, &ResourceDownloadDialog::confirm); auto CancelButton = m_buttons.button(QDialogButtonBox::Cancel); @@ -112,23 +123,79 @@ void ResourceDownloadDialog::connectButtons() connect(HelpButton, &QPushButton::clicked, m_container, &PageContainer::help); } +static ModPlatform::ProviderCapabilities ProviderCaps; + +QStringList getRequiredBy(QList tasks, ResourceDownloadDialog::DownloadTaskPtr pack) +{ + auto addonId = pack->getPack()->addonId; + auto provider = pack->getPack()->provider; + auto version = pack->getVersionID(); + auto req = QStringList(); + for (auto& task : tasks) { + if (provider != task->getPack()->provider) + continue; + auto deps = task->getVersion().dependencies; + if (auto dep = std::find_if(deps.begin(), deps.end(), + [addonId, provider, version](const ModPlatform::Dependency& d) { + return d.type == ModPlatform::DependencyType::REQUIRED && + (provider == ModPlatform::ResourceProvider::MODRINTH && d.addonId.toString().isEmpty() + ? version == d.version + : d.addonId == addonId); + }); + dep != deps.end()) { + req.append(task->getName()); + } + } + return req; +} + void ResourceDownloadDialog::confirm() { - auto keys = m_selected.keys(); - keys.sort(Qt::CaseInsensitive); - auto confirm_dialog = ReviewMessageBox::create(this, tr("Confirm %1 to download").arg(resourcesString())); confirm_dialog->retranslateUi(resourcesString()); - for (auto& task : keys) { - auto selected = m_selected.constFind(task).value(); - confirm_dialog->appendResource({ task, selected->getFilename(), selected->getCustomPath() }); + if (auto task = getModDependenciesTask(); task) { + connect(task.get(), &Task::failed, this, + [&](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); + + connect(task.get(), &Task::succeeded, this, [&]() { + QStringList warnings = task->warnings(); + if (warnings.count()) { + CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->exec(); + } + }); + + // Check for updates + ProgressDialog progress_dialog(this); + progress_dialog.setSkipButton(true, tr("Abort")); + progress_dialog.setWindowTitle(tr("Checking for dependencies...")); + auto ret = progress_dialog.execWithTask(task.get()); + + // If the dialog was skipped / some download error happened + if (ret == QDialog::DialogCode::Rejected) { + QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection); + return; + } else { + for (auto dep : task->getDependecies()) + addResource(dep->pack, dep->version); + } + } + + auto selected = getTasks(); + std::sort(selected.begin(), selected.end(), [](const DownloadTaskPtr& a, const DownloadTaskPtr& b) { + return QString::compare(a->getName(), b->getName(), Qt::CaseInsensitive) < 0; + }); + for (auto& task : selected) { + confirm_dialog->appendResource({ task->getName(), task->getFilename(), task->getCustomPath(), + ProviderCaps.name(task->getProvider()), getRequiredBy(selected, task) }); } if (confirm_dialog->exec()) { auto deselected = confirm_dialog->deselectedResources(); - for (auto name : deselected) { - m_selected.remove(name); + for (auto page : m_container->getPages()) { + auto res = static_cast(page); + for (auto name : deselected) + res->removeResourceFromPage(name); } this->accept(); @@ -145,46 +212,39 @@ ResourcePage* ResourceDownloadDialog::getSelectedPage() return m_selectedPage; } -void ResourceDownloadDialog::addResource(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, bool is_indexed) +void ResourceDownloadDialog::addResource(ModPlatform::IndexedPack::Ptr pack, ModPlatform::IndexedVersion& ver) { - removeResource(pack, ver); - - ver.is_currently_selected = true; - m_selected.insert(pack.name, makeShared(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; -} - -void ResourceDownloadDialog::removeResource(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver) -{ - if (auto selected_task_it = m_selected.find(pack.name); selected_task_it != m_selected.end()) { - auto selected_task = *selected_task_it; - auto old_version_id = selected_task->getVersionID(); - - // If the new and old version IDs don't match, search for the old one and deselect it. - if (ver.fileId != old_version_id) - getVersionWithID(pack, old_version_id).is_currently_selected = false; + for (auto page : m_container->getPages()) { + static_cast(page)->removeResourceFromPage(pack_name); } + setButtonStatus(); +} - // 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()); +void ResourceDownloadDialog::setButtonStatus() +{ + auto selected = false; + for (auto page : m_container->getPages()) { + auto res = static_cast(page); + selected = selected || res->hasSelectedPacks(); + } + m_buttons.button(QDialogButtonBox::Ok)->setEnabled(selected); } const QList ResourceDownloadDialog::getTasks() { - return m_selected.values(); + QList selected; + for (auto page : m_container->getPages()) { + auto res = static_cast(page); + selected.append(res->selectedPacks()); + } + return selected; } void ResourceDownloadDialog::selectedPageChanged(BasePage* previous, BasePage* selected) @@ -205,8 +265,6 @@ void ResourceDownloadDialog::selectedPageChanged(BasePage* previous, BasePage* s m_selectedPage->setSearchTerm(prev_page->getSearchTerm()); } - - ModDownloadDialog::ModDownloadDialog(QWidget* parent, const std::shared_ptr& mods, BaseInstance* instance) : ResourceDownloadDialog(parent, mods), m_instance(instance) { @@ -232,6 +290,18 @@ QList ModDownloadDialog::getPages() return pages; } +GetModDependenciesTask::Ptr ModDownloadDialog::getModDependenciesTask() +{ + if (auto model = dynamic_cast(getBaseModel().get()); model) { + QList> selectedVers; + for (auto& selected : getTasks()) { + selectedVers.append(std::make_shared(selected->getPack(), selected->getVersion())); + } + + return makeShared(this, m_instance, model, selectedVers); + } + return nullptr; +}; ResourcePackDownloadDialog::ResourcePackDownloadDialog(QWidget* parent, const std::shared_ptr& resource_packs, @@ -255,10 +325,11 @@ QList ResourcePackDownloadDialog::getPages() if (APPLICATION->capabilities() & Application::SupportsFlame) pages.append(FlameResourcePackPage::create(this, *m_instance)); + m_selectedPage = dynamic_cast(pages[0]); + return pages; } - TexturePackDownloadDialog::TexturePackDownloadDialog(QWidget* parent, const std::shared_ptr& resource_packs, BaseInstance* instance) @@ -281,10 +352,11 @@ QList TexturePackDownloadDialog::getPages() if (APPLICATION->capabilities() & Application::SupportsFlame) pages.append(FlameTexturePackPage::create(this, *m_instance)); + m_selectedPage = dynamic_cast(pages[0]); + return pages; } - ShaderPackDownloadDialog::ShaderPackDownloadDialog(QWidget* parent, const std::shared_ptr& shaders, BaseInstance* instance) @@ -305,6 +377,8 @@ QList ShaderPackDownloadDialog::getPages() pages.append(ModrinthShaderPackPage::create(this, *m_instance)); + m_selectedPage = dynamic_cast(pages[0]); + return pages; } diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.h b/launcher/ui/dialogs/ResourceDownloadDialog.h index 5678dc8bb..f65daaa3f 100644 --- a/launcher/ui/dialogs/ResourceDownloadDialog.h +++ b/launcher/ui/dialogs/ResourceDownloadDialog.h @@ -25,6 +25,7 @@ #include #include "QObjectPtr.h" +#include "minecraft/mod/tasks/GetModDependenciesTask.h" #include "modplatform/ModIndex.h" #include "ui/pages/BasePageProvider.h" @@ -62,8 +63,8 @@ class ResourceDownloadDialog : public QDialog, public BasePageProvider { bool selectPage(QString pageId); ResourcePage* getSelectedPage(); - void addResource(ModPlatform::IndexedPack&, ModPlatform::IndexedVersion&, bool is_indexed = false); - void removeResource(ModPlatform::IndexedPack&, ModPlatform::IndexedVersion&); + void addResource(ModPlatform::IndexedPack::Ptr, ModPlatform::IndexedVersion&); + void removeResource(const QString&); const QList getTasks(); [[nodiscard]] const std::shared_ptr getBaseModel() const { return m_base_model; } @@ -79,6 +80,9 @@ class ResourceDownloadDialog : public QDialog, public BasePageProvider { protected: [[nodiscard]] virtual QString geometrySaveKey() const { return ""; } + void setButtonStatus(); + + [[nodiscard]] virtual GetModDependenciesTask::Ptr getModDependenciesTask() { return nullptr; } protected: const std::shared_ptr m_base_model; @@ -88,12 +92,8 @@ class ResourceDownloadDialog : public QDialog, public BasePageProvider { QDialogButtonBox m_buttons; QVBoxLayout m_vertical_layout; - - QHash m_selected; }; - - class ModDownloadDialog final : public ResourceDownloadDialog { Q_OBJECT @@ -106,6 +106,7 @@ class ModDownloadDialog final : public ResourceDownloadDialog { [[nodiscard]] QString geometrySaveKey() const override { return "ModDownloadGeometry"; } QList getPages() override; + GetModDependenciesTask::Ptr getModDependenciesTask() override; private: BaseInstance* m_instance; @@ -135,8 +136,8 @@ class TexturePackDownloadDialog final : public ResourceDownloadDialog { public: explicit TexturePackDownloadDialog(QWidget* parent, - const std::shared_ptr& resource_packs, - BaseInstance* instance); + const std::shared_ptr& resource_packs, + BaseInstance* instance); ~TexturePackDownloadDialog() override = default; //: String that gets appended to the texture pack download dialog title ("Download " + resourcesString()) @@ -153,9 +154,7 @@ class ShaderPackDownloadDialog final : public ResourceDownloadDialog { Q_OBJECT public: - explicit ShaderPackDownloadDialog(QWidget* parent, - const std::shared_ptr& shader_packs, - BaseInstance* instance); + explicit ShaderPackDownloadDialog(QWidget* parent, const std::shared_ptr& shader_packs, BaseInstance* instance); ~ShaderPackDownloadDialog() override = default; //: String that gets appended to the shader pack download dialog title ("Download " + resourcesString()) diff --git a/launcher/ui/dialogs/ReviewMessageBox.cpp b/launcher/ui/dialogs/ReviewMessageBox.cpp index 7b2df2780..7b33765fd 100644 --- a/launcher/ui/dialogs/ReviewMessageBox.cpp +++ b/launcher/ui/dialogs/ReviewMessageBox.cpp @@ -40,7 +40,8 @@ void ReviewMessageBox::appendResource(ResourceInformation&& info) auto filenameItem = new QTreeWidgetItem(itemTop); filenameItem->setText(0, tr("Filename: %1").arg(info.filename)); - itemTop->insertChildren(0, { filenameItem }); + auto childIndx = 0; + itemTop->insertChildren(childIndx++, { filenameItem }); if (!info.custom_file_path.isEmpty()) { auto customPathItem = new QTreeWidgetItem(itemTop); @@ -49,7 +50,31 @@ void ReviewMessageBox::appendResource(ResourceInformation&& info) itemTop->insertChildren(1, { customPathItem }); itemTop->setIcon(1, QIcon(APPLICATION->getThemedIcon("status-yellow"))); - itemTop->setToolTip(1, tr("This file will be downloaded to a folder location different from the default, possibly due to its loader requiring it.")); + itemTop->setToolTip( + childIndx++, + tr("This file will be downloaded to a folder location different from the default, possibly due to its loader requiring it.")); + } + + auto providerItem = new QTreeWidgetItem(itemTop); + providerItem->setText(0, tr("Provider: %1").arg(info.provider)); + + itemTop->insertChildren(childIndx++, { providerItem }); + + if (!info.required_by.isEmpty()) { + auto requiredByItem = new QTreeWidgetItem(itemTop); + if (info.required_by.length() == 1) { + requiredByItem->setText(0, tr("Required by: %1").arg(info.required_by.back())); + } else { + requiredByItem->setText(0, tr("Required by:")); + auto i = 0; + for (auto req : info.required_by) { + auto reqItem = new QTreeWidgetItem(requiredByItem); + reqItem->setText(0, req); + reqItem->insertChildren(i++, { reqItem }); + } + } + + itemTop->insertChildren(childIndx++, { requiredByItem }); } ui->modTreeWidget->addTopLevelItem(itemTop); diff --git a/launcher/ui/dialogs/ReviewMessageBox.h b/launcher/ui/dialogs/ReviewMessageBox.h index 5ec2bc231..a520cc2a6 100644 --- a/launcher/ui/dialogs/ReviewMessageBox.h +++ b/launcher/ui/dialogs/ReviewMessageBox.h @@ -13,9 +13,11 @@ class ReviewMessageBox : public QDialog { static auto create(QWidget* parent, QString&& title, QString&& icon = "") -> ReviewMessageBox*; using ResourceInformation = struct res_info { - QString name; - QString filename; - QString custom_file_path {}; + QString name; + QString filename; + QString custom_file_path{}; + QString provider; + QStringList required_by; }; void appendResource(ResourceInformation&& info); diff --git a/launcher/ui/dialogs/VersionSelectDialog.cpp b/launcher/ui/dialogs/VersionSelectDialog.cpp index d7880334f..5feb70d20 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 * - * 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 . + * + * 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 #include -#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 ceb240401..dc2bde992 100644 --- a/launcher/ui/pages/BasePage.h +++ b/launcher/ui/pages/BasePage.h @@ -35,15 +35,16 @@ #pragma once -#include #include +#include +#include #include #include "BasePageContainer.h" -class BasePage -{ -public: +class BasePage { + public: + using updateExtraInfoFunc = std::function; 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 f662ee1c3..dca1b3a63 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 55bd3eea7..d9116bfcf 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -172,7 +172,7 @@ Disable using metadata provided by mod providers (like Modrinth or Curseforge) for mods.
- Disable using metadata for mods? + Disable using metadata for mods
@@ -307,21 +307,21 @@ - Show console while the game is &running? + Show console while the game is &running - &Automatically close console when the game quits? + &Automatically close console when the game quits - Show console when the game &crashes? + Show console when the game &crashes diff --git a/launcher/ui/pages/global/MinecraftPage.ui b/launcher/ui/pages/global/MinecraftPage.ui index 103881b57..8f5de725d 100644 --- a/launcher/ui/pages/global/MinecraftPage.ui +++ b/launcher/ui/pages/global/MinecraftPage.ui @@ -51,7 +51,7 @@ - Start Minecraft &maximized? + Start Minecraft &maximized diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.cpp b/launcher/ui/pages/instance/ExternalResourcesPage.cpp index 1115ddc3b..173bcb663 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 + * + * 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 . + * + * 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 #include +#include ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared_ptr model, QWidget* parent) : QMainWindow(parent), m_instance(instance), ui(new Ui::ExternalResourcesPage), m_model(model) @@ -24,6 +60,8 @@ ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared m_filterModel->setSourceModel(m_model.get()); m_filterModel->setFilterKeyColumn(-1); ui->treeView->setModel(m_filterModel); + // must come after setModel + ui->treeView->setResizeModes(m_model->columnResizeModes()); ui->treeView->installEventFilter(this); ui->treeView->sortByColumn(1, Qt::AscendingOrder); @@ -43,7 +81,21 @@ ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared auto selection_model = ui->treeView->selectionModel(); connect(selection_model, &QItemSelectionModel::currentChanged, this, &ExternalResourcesPage::current); + auto updateExtra = [this]() { + if (updateExtraInfo) + updateExtraInfo(id(), extraHeaderInfoString()); + }; + connect(selection_model, &QItemSelectionModel::selectionChanged, this, updateExtra); + connect(model.get(), &ResourceFolderModel::updateFinished, this, updateExtra); + connect(ui->filterEdit, &QLineEdit::textChanged, this, &ExternalResourcesPage::filterTextChanged); + + auto viewHeader = ui->treeView->header(); + viewHeader->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(viewHeader, &QHeaderView::customContextMenuRequested, this, &ExternalResourcesPage::ShowHeaderContextMenu); + + m_model->loadHiddenColumns(ui->treeView); } ExternalResourcesPage::~ExternalResourcesPage() @@ -65,6 +117,13 @@ void ExternalResourcesPage::ShowContextMenu(const QPoint& pos) delete menu; } +void ExternalResourcesPage::ShowHeaderContextMenu(const QPoint& pos) +{ + auto menu = m_model->createHeaderContextMenu(ui->treeView); + menu->exec(ui->treeView->mapToGlobal(pos)); + menu->deleteLater(); +} + void ExternalResourcesPage::openedImpl() { m_model->startWatching(); @@ -96,7 +155,6 @@ void ExternalResourcesPage::itemActivated(const QModelIndex&) return; auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()); - m_model->setResourceEnabled(selection.indexes(), EnableAction::TOGGLE); } void ExternalResourcesPage::filterTextChanged(const QString& newContents) @@ -248,6 +306,15 @@ bool ExternalResourcesPage::onSelectionChanged(const QModelIndex& current, const int row = sourceCurrent.row(); Resource const& resource = m_model->at(row); ui->frame->updateWithResource(resource); - return true; } + +QString ExternalResourcesPage::extraHeaderInfoString() +{ + if (ui && ui->treeView && ui->treeView->selectionModel()) { + auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); + if (auto count = std::count_if(selection.cbegin(), selection.cend(), [](auto v) { return v.column() == 0; }); count != 0) + return tr(" (%1 installed, %2 selected)").arg(m_model->size()).arg(count); + } + return tr(" (%1 installed)").arg(m_model->size()); +} diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.h b/launcher/ui/pages/instance/ExternalResourcesPage.h index d17fbb7f1..6c0a12cb8 100644 --- a/launcher/ui/pages/instance/ExternalResourcesPage.h +++ b/launcher/ui/pages/instance/ExternalResourcesPage.h @@ -29,6 +29,7 @@ class ExternalResourcesPage : public QMainWindow, public BasePage { virtual QString helpPage() const override = 0; virtual bool shouldDisplay() const override = 0; + QString extraHeaderInfoString(); void openedImpl() override; void closedImpl() override; @@ -60,6 +61,7 @@ class ExternalResourcesPage : public QMainWindow, public BasePage { virtual void viewConfigs(); void ShowContextMenu(const QPoint& pos); + void ShowHeaderContextMenu(const QPoint& pos); protected: BaseInstance* m_instance = nullptr; diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.ui b/launcher/ui/pages/instance/ExternalResourcesPage.ui index 33a033366..f676361c3 100644 --- a/launcher/ui/pages/instance/ExternalResourcesPage.ui +++ b/launcher/ui/pages/instance/ExternalResourcesPage.ui @@ -62,6 +62,9 @@ QAbstractItemView::DropOnly + + true + diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.cpp b/launcher/ui/pages/instance/InstanceSettingsPage.cpp index 4b4c73dc6..2a7c5b271 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::of(&QComboBox::currentIndexChanged), this, &InstanceSettingsPage::changeInstanceAccount); loadSettings(); - updateThresholds(); -} -bool InstanceSettingsPage::shouldDisplay() const -{ - return !m_instance->isRunning(); + + updateThresholds(); } InstanceSettingsPage::~InstanceSettingsPage() @@ -88,12 +85,12 @@ void InstanceSettingsPage::globalSettingsButtonClicked(bool) case 0: APPLICATION->ShowGlobalSettings(this, "java-settings"); return; - case 1: - APPLICATION->ShowGlobalSettings(this, "minecraft-settings"); - return; case 2: APPLICATION->ShowGlobalSettings(this, "custom-commands"); return; + default: + APPLICATION->ShowGlobalSettings(this, "minecraft-settings"); + return; } } @@ -454,36 +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 cb6fbae0c..0438fe3b2 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 checker; - QMenu *accountMenu = nullptr; }; diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.ui b/launcher/ui/pages/instance/InstanceSettingsPage.ui index 1b9861842..8427965de 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.ui +++ b/launcher/ui/pages/instance/InstanceSettingsPage.ui @@ -269,7 +269,7 @@ - Start Minecraft maximized? + Start Minecraft maximized @@ -341,21 +341,21 @@ - Show console while the game is running? + Show console while the game is running - Automatically close console when the game quits? + Automatically close console when the game quits - Show console when the game crashes? + Show console when the game crashes @@ -636,14 +636,7 @@ - - - QToolButton::InstantPopup - - - Qt::ToolButtonTextBesideIcon - - + diff --git a/launcher/ui/pages/instance/ManagedPackPage.cpp b/launcher/ui/pages/instance/ManagedPackPage.cpp index d0701a7ad..d89c5bfc0 100644 --- a/launcher/ui/pages/instance/ManagedPackPage.cpp +++ b/launcher/ui/pages/instance/ManagedPackPage.cpp @@ -205,7 +205,7 @@ ModrinthManagedPackPage::ModrinthManagedPackPage(BaseInstance* inst, InstanceWin { Q_ASSERT(inst->isManagedPack()); connect(ui->versionsComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(suggestVersion())); - connect(ui->updateButton, &QPushButton::pressed, this, &ModrinthManagedPackPage::update); + connect(ui->updateButton, &QPushButton::clicked, this, &ModrinthManagedPackPage::update); } // MODRINTH @@ -226,7 +226,7 @@ void ModrinthManagedPackPage::parseManagedPack() QString id = m_inst->getManagedPackID(); - m_fetch_job->addNetAction(Net::Download::makeByteArray(QString("%1/project/%2/version").arg(BuildConfig.MODRINTH_PROD_URL, id), response.get())); + m_fetch_job->addNetAction(Net::Download::makeByteArray(QString("%1/project/%2/version").arg(BuildConfig.MODRINTH_PROD_URL, id), response)); QObject::connect(m_fetch_job.get(), &NetJob::succeeded, this, [this, response, id] { QJsonParseError parse_error{}; @@ -332,7 +332,7 @@ FlameManagedPackPage::FlameManagedPackPage(BaseInstance* inst, InstanceWindow* i { Q_ASSERT(inst->isManagedPack()); connect(ui->versionsComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(suggestVersion())); - connect(ui->updateButton, &QPushButton::pressed, this, &FlameManagedPackPage::update); + connect(ui->updateButton, &QPushButton::clicked, this, &FlameManagedPackPage::update); } void FlameManagedPackPage::parseManagedPack() @@ -369,7 +369,7 @@ void FlameManagedPackPage::parseManagedPack() QString id = m_inst->getManagedPackID(); - m_fetch_job->addNetAction(Net::Download::makeByteArray(QString("%1/mods/%2/files").arg(BuildConfig.FLAME_BASE_URL, id), response.get())); + m_fetch_job->addNetAction(Net::Download::makeByteArray(QString("%1/mods/%2/files").arg(BuildConfig.FLAME_BASE_URL, id), response)); QObject::connect(m_fetch_job.get(), &NetJob::succeeded, this, [this, response, id] { QJsonParseError parse_error{}; diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index 4548af59a..90e7d0d62 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -4,6 +4,7 @@ * Copyright (c) 2022 Jamie Mansfield * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2022 TheKodeToad + * Copyright (c) 2023 Trial97 * * 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 connect(ui->actionUpdateItem, &QAction::triggered, this, &ModFolderPage::updateMods); auto check_allow_update = [this] { - return (!m_instance || !m_instance->isRunning()) && - (ui->treeView->selectionModel()->hasSelection() || !m_model->empty()); + return (!m_instance || !m_instance->isRunning()) && (ui->treeView->selectionModel()->hasSelection() || !m_model->empty()); }; - connect(ui->treeView->selectionModel(), &QItemSelectionModel::selectionChanged, this, [this, check_allow_update] { - ui->actionUpdateItem->setEnabled(check_allow_update()); - }); + connect(ui->treeView->selectionModel(), &QItemSelectionModel::selectionChanged, this, + [this, check_allow_update] { ui->actionUpdateItem->setEnabled(check_allow_update()); }); - connect(mods.get(), &ModFolderModel::rowsInserted, this, [this, check_allow_update] { - ui->actionUpdateItem->setEnabled(check_allow_update()); - }); + connect(mods.get(), &ModFolderModel::rowsInserted, this, + [this, check_allow_update] { ui->actionUpdateItem->setEnabled(check_allow_update()); }); - connect(mods.get(), &ModFolderModel::rowsRemoved, this, [this, check_allow_update] { - ui->actionUpdateItem->setEnabled(check_allow_update()); - }); + connect(mods.get(), &ModFolderModel::rowsRemoved, this, + [this, check_allow_update] { ui->actionUpdateItem->setEnabled(check_allow_update()); }); - connect(mods.get(), &ModFolderModel::updateFinished, this, [this, check_allow_update, mods] { - ui->actionUpdateItem->setEnabled(check_allow_update()); - - // Prevent a weird crash when trying to open the mods page twice in a session o.O - disconnect(mods.get(), &ModFolderModel::updateFinished, this, 0); - }); + connect(mods.get(), &ModFolderModel::updateFinished, this, + [this, check_allow_update] { ui->actionUpdateItem->setEnabled(check_allow_update()); }); connect(m_instance, &BaseInstance::runningStatusChanged, this, &ModFolderPage::runningStateChanged); ModFolderPage::runningStateChanged(m_instance && m_instance->isRunning()); diff --git a/launcher/ui/pages/instance/ScreenshotsPage.cpp b/launcher/ui/pages/instance/ScreenshotsPage.cpp index ca368d3b7..352375941 100644 --- a/launcher/ui/pages/instance/ScreenshotsPage.cpp +++ b/launcher/ui/pages/instance/ScreenshotsPage.cpp @@ -36,6 +36,7 @@ */ #include "ScreenshotsPage.h" +#include "BuildConfig.h" #include "ui_ScreenshotsPage.h" #include @@ -380,16 +381,18 @@ void ScreenshotsPage::on_actionUpload_triggered() if (selection.isEmpty()) return; - QString text; + QUrl baseUrl(BuildConfig.IMGUR_BASE_URL); if (selection.size() > 1) - text = tr("You are about to upload %1 screenshots.\n\n" + text = tr("You are about to upload %1 screenshots to %2.\n" + "You should double-check for personal information.\n\n" "Are you sure?") - .arg(selection.size()); + .arg(QString::number(selection.size()), baseUrl.host()); else - text = - tr("You are about to upload the selected screenshot.\n\n" - "Are you sure?"); + text = tr("You are about to upload the selected screenshot to %1.\n" + "You should double-check for personal information.\n\n" + "Are you sure?") + .arg(baseUrl.host()); auto response = CustomMessageBox::selectable(this, "Confirm Upload", text, QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) diff --git a/launcher/ui/pages/instance/VersionPage.cpp b/launcher/ui/pages/instance/VersionPage.cpp index 74b7ec7c2..59107c53a 100644 --- a/launcher/ui/pages/instance/VersionPage.cpp +++ b/launcher/ui/pages/instance/VersionPage.cpp @@ -287,7 +287,6 @@ void VersionPage::updateButtons(int row) ui->actionAdd_Empty->setEnabled(controlsEnabled); ui->actionImport_Components->setEnabled(controlsEnabled); ui->actionReload->setEnabled(controlsEnabled); - ui->actionInstall_mods->setEnabled(controlsEnabled); ui->actionReplace_Minecraft_jar->setEnabled(controlsEnabled); ui->actionAdd_to_Minecraft_jar->setEnabled(controlsEnabled); ui->actionAdd_Agents->setEnabled(controlsEnabled); diff --git a/launcher/ui/pages/instance/VersionPage.ui b/launcher/ui/pages/instance/VersionPage.ui index 4777eafe0..a73c42d6c 100644 --- a/launcher/ui/pages/instance/VersionPage.ui +++ b/launcher/ui/pages/instance/VersionPage.ui @@ -102,7 +102,6 @@ - @@ -112,7 +111,6 @@ - @@ -204,14 +202,6 @@ Install the LiteLoader package. - - - Install mods - - - Install normal mods. - - Add to Minecraft.jar diff --git a/launcher/ui/pages/modplatform/VanillaPage.cpp b/launcher/ui/pages/modplatform/CustomPage.cpp similarity index 79% rename from launcher/ui/pages/modplatform/VanillaPage.cpp rename to launcher/ui/pages/modplatform/CustomPage.cpp index 29fecb854..e164171a4 100644 --- a/launcher/ui/pages/modplatform/VanillaPage.cpp +++ b/launcher/ui/pages/modplatform/CustomPage.cpp @@ -33,8 +33,8 @@ * limitations under the License. */ -#include "VanillaPage.h" -#include "ui_VanillaPage.h" +#include "CustomPage.h" +#include "ui_CustomPage.h" #include @@ -46,32 +46,32 @@ #include "minecraft/VanillaInstanceCreationTask.h" #include "ui/dialogs/NewInstanceDialog.h" -VanillaPage::VanillaPage(NewInstanceDialog *dialog, QWidget *parent) - : QWidget(parent), dialog(dialog), ui(new Ui::VanillaPage) +CustomPage::CustomPage(NewInstanceDialog *dialog, QWidget *parent) + : QWidget(parent), dialog(dialog), ui(new Ui::CustomPage) { ui->setupUi(this); ui->tabWidget->tabBar()->hide(); - connect(ui->versionList, &VersionSelectWidget::selectedVersionChanged, this, &VanillaPage::setSelectedVersion); + connect(ui->versionList, &VersionSelectWidget::selectedVersionChanged, this, &CustomPage::setSelectedVersion); filterChanged(); - connect(ui->alphaFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged); - connect(ui->betaFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged); - connect(ui->snapshotFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged); - connect(ui->oldSnapshotFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged); - connect(ui->releaseFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged); - connect(ui->experimentsFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged); - connect(ui->refreshBtn, &QPushButton::clicked, this, &VanillaPage::refresh); + connect(ui->alphaFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged); + connect(ui->betaFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged); + connect(ui->snapshotFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged); + connect(ui->oldSnapshotFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged); + connect(ui->releaseFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged); + connect(ui->experimentsFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged); + connect(ui->refreshBtn, &QPushButton::clicked, this, &CustomPage::refresh); - connect(ui->loaderVersionList, &VersionSelectWidget::selectedVersionChanged, this, &VanillaPage::setSelectedLoaderVersion); - connect(ui->noneFilter, &QRadioButton::toggled, this, &VanillaPage::loaderFilterChanged); - connect(ui->forgeFilter, &QRadioButton::toggled, this, &VanillaPage::loaderFilterChanged); - connect(ui->fabricFilter, &QRadioButton::toggled, this, &VanillaPage::loaderFilterChanged); - connect(ui->quiltFilter, &QRadioButton::toggled, this, &VanillaPage::loaderFilterChanged); - connect(ui->liteLoaderFilter, &QRadioButton::toggled, this, &VanillaPage::loaderFilterChanged); - connect(ui->loaderRefreshBtn, &QPushButton::clicked, this, &VanillaPage::loaderRefresh); + connect(ui->loaderVersionList, &VersionSelectWidget::selectedVersionChanged, this, &CustomPage::setSelectedLoaderVersion); + connect(ui->noneFilter, &QRadioButton::toggled, this, &CustomPage::loaderFilterChanged); + connect(ui->forgeFilter, &QRadioButton::toggled, this, &CustomPage::loaderFilterChanged); + connect(ui->fabricFilter, &QRadioButton::toggled, this, &CustomPage::loaderFilterChanged); + connect(ui->quiltFilter, &QRadioButton::toggled, this, &CustomPage::loaderFilterChanged); + connect(ui->liteLoaderFilter, &QRadioButton::toggled, this, &CustomPage::loaderFilterChanged); + connect(ui->loaderRefreshBtn, &QPushButton::clicked, this, &CustomPage::loaderRefresh); } -void VanillaPage::openedImpl() +void CustomPage::openedImpl() { if(!initialized) { @@ -85,19 +85,19 @@ void VanillaPage::openedImpl() } } -void VanillaPage::refresh() +void CustomPage::refresh() { ui->versionList->loadList(); } -void VanillaPage::loaderRefresh() +void CustomPage::loaderRefresh() { if(ui->noneFilter->isChecked()) return; ui->loaderVersionList->loadList(); } -void VanillaPage::filterChanged() +void CustomPage::filterChanged() { QStringList out; if(ui->alphaFilter->isChecked()) @@ -116,7 +116,7 @@ void VanillaPage::filterChanged() ui->versionList->setFilter(BaseVersionList::TypeRole, new RegexpFilter(regexp, false)); } -void VanillaPage::loaderFilterChanged() +void CustomPage::loaderFilterChanged() { QString minecraftVersion; if (m_selectedVersion) @@ -172,37 +172,37 @@ void VanillaPage::loaderFilterChanged() ui->loaderVersionList->setEmptyString(tr("No versions are currently available for Minecraft %1").arg(minecraftVersion)); } -VanillaPage::~VanillaPage() +CustomPage::~CustomPage() { delete ui; } -bool VanillaPage::shouldDisplay() const +bool CustomPage::shouldDisplay() const { return true; } -void VanillaPage::retranslate() +void CustomPage::retranslate() { ui->retranslateUi(this); } -BaseVersion::Ptr VanillaPage::selectedVersion() const +BaseVersion::Ptr CustomPage::selectedVersion() const { return m_selectedVersion; } -BaseVersion::Ptr VanillaPage::selectedLoaderVersion() const +BaseVersion::Ptr CustomPage::selectedLoaderVersion() const { return m_selectedLoaderVersion; } -QString VanillaPage::selectedLoader() const +QString CustomPage::selectedLoader() const { return m_selectedLoader; } -void VanillaPage::suggestCurrent() +void CustomPage::suggestCurrent() { if (!isOpened) { @@ -227,14 +227,14 @@ void VanillaPage::suggestCurrent() dialog->setSuggestedIcon("default"); } -void VanillaPage::setSelectedVersion(BaseVersion::Ptr version) +void CustomPage::setSelectedVersion(BaseVersion::Ptr version) { m_selectedVersion = version; suggestCurrent(); loaderFilterChanged(); } -void VanillaPage::setSelectedLoaderVersion(BaseVersion::Ptr version) +void CustomPage::setSelectedLoaderVersion(BaseVersion::Ptr version) { m_selectedLoaderVersion = version; suggestCurrent(); diff --git a/launcher/ui/pages/modplatform/VanillaPage.h b/launcher/ui/pages/modplatform/CustomPage.h similarity index 92% rename from launcher/ui/pages/modplatform/VanillaPage.h rename to launcher/ui/pages/modplatform/CustomPage.h index 39aba760b..8b5a5011c 100644 --- a/launcher/ui/pages/modplatform/VanillaPage.h +++ b/launcher/ui/pages/modplatform/CustomPage.h @@ -43,21 +43,21 @@ namespace Ui { -class VanillaPage; +class CustomPage; } class NewInstanceDialog; -class VanillaPage : public QWidget, public BasePage +class CustomPage : public QWidget, public BasePage { Q_OBJECT public: - explicit VanillaPage(NewInstanceDialog *dialog, QWidget *parent = 0); - virtual ~VanillaPage(); + explicit CustomPage(NewInstanceDialog *dialog, QWidget *parent = 0); + virtual ~CustomPage(); virtual QString displayName() const override { - return tr("Vanilla"); + return tr("Custom"); } virtual QIcon icon() const override { @@ -96,7 +96,7 @@ private: private: bool initialized = false; NewInstanceDialog *dialog = nullptr; - Ui::VanillaPage *ui = nullptr; + Ui::CustomPage *ui = nullptr; bool m_versionSetByUser = false; BaseVersion::Ptr m_selectedVersion; BaseVersion::Ptr m_selectedLoaderVersion; diff --git a/launcher/ui/pages/modplatform/VanillaPage.ui b/launcher/ui/pages/modplatform/CustomPage.ui similarity index 99% rename from launcher/ui/pages/modplatform/VanillaPage.ui rename to launcher/ui/pages/modplatform/CustomPage.ui index 431109273..0d89b5956 100644 --- a/launcher/ui/pages/modplatform/VanillaPage.ui +++ b/launcher/ui/pages/modplatform/CustomPage.ui @@ -1,7 +1,7 @@ - VanillaPage - + CustomPage + 0 diff --git a/launcher/ui/pages/modplatform/ImportPage.h b/launcher/ui/pages/modplatform/ImportPage.h index 8d13ac100..c2acb92da 100644 --- a/launcher/ui/pages/modplatform/ImportPage.h +++ b/launcher/ui/pages/modplatform/ImportPage.h @@ -57,7 +57,7 @@ public: virtual ~ImportPage(); virtual QString displayName() const override { - return tr("Import from zip"); + return tr("Import"); } virtual QIcon icon() const override { diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp index afd8b2921..b75378905 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 +#include 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> 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(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 5d4a77859..dd187aa8d 100644 --- a/launcher/ui/pages/modplatform/ModModel.h +++ b/launcher/ui/pages/modplatform/ModModel.h @@ -24,7 +24,7 @@ class ModModel : public ResourceModel { Q_OBJECT public: - ModModel(const BaseInstance&, ResourceAPI* api); + ModModel(BaseInstance&, ResourceAPI* api); /* Ask the API for more information */ void searchWithTerm(const QString& term, unsigned int sort, bool filter_changed); @@ -32,6 +32,7 @@ class ModModel : public ResourceModel { void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override = 0; void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override = 0; void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override = 0; + virtual ModPlatform::IndexedVersion loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) = 0; void setFilter(std::shared_ptr filter) { m_filter = filter; } @@ -42,9 +43,10 @@ class ModModel : public ResourceModel { protected: auto documentToArray(QJsonDocument& obj) const -> QJsonArray override = 0; + virtual bool isPackInstalled(ModPlatform::IndexedPack::Ptr) const override; protected: - const BaseInstance& m_base_instance; + BaseInstance& m_base_instance; std::shared_ptr m_filter = nullptr; }; diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index 04be43ada..60a43128a 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& widget) m_filter_widget->setInstance(&static_cast(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(m_model)->searchWithTerm(getSearchTerm(), m_ui->sortByBox->currentData().toUInt(), changed); + static_cast(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 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 4ea55efa3..5510c1911 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 + template static T* create(ModDownloadDialog* dialog, BaseInstance& instance) { auto page = new T(dialog, instance); auto model = static_cast(page->getModel()); - auto filter_widget = ModFilterWidget::create(static_cast(instance).getPackProfile()->getComponentVersion("net.minecraft"), page); + auto filter_widget = + ModFilterWidget::create(static_cast(instance).getPackProfile()->getComponentVersion("net.minecraft"), page); page->setFilterWidget(filter_widget); model->setFilter(page->getFilter()); @@ -48,9 +49,13 @@ class ModPage : public ResourcePage { [[nodiscard]] QMap urlHandlers() const override; - void addResourceToDialog(ModPlatform::IndexedPack&, ModPlatform::IndexedVersion&) override; + void addResourceToPage(ModPlatform::IndexedPack::Ptr, + ModPlatform::IndexedVersion&, + const std::shared_ptr) override; - virtual auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, std::optional loaders = {}) const -> bool = 0; + virtual auto validateVersion(ModPlatform::IndexedVersion& ver, + QString mineVer, + std::optional loaders = {}) const -> bool = 0; [[nodiscard]] bool supportsFiltering() const override { return true; }; auto getFilter() const -> const std::shared_ptr { return m_filter; } diff --git a/launcher/ui/pages/modplatform/ResourceModel.cpp b/launcher/ui/pages/modplatform/ResourceModel.cpp index 472aa8515..49405a02b 100644 --- a/launcher/ui/pages/modplatform/ResourceModel.cpp +++ b/launcher/ui/pages/modplatform/ResourceModel.cpp @@ -6,9 +6,11 @@ #include #include +#include #include #include #include +#include #include #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 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(value.value()); + m_packs[pos] = value.value(); 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(); 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(); + auto current_pack = data(index, Qt::UserRole).value(); // 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(); + auto current_pack = data(index, Qt::UserRole).value(); // 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 packs, + bool is_indexed, + QString custom_target_folder) +{ + version.is_currently_selected = true; + m_selected.append(makeShared(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 1ec42cda8..6533d9c6a 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; + 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 getIcon(QModelIndex&, const QUrl&); + void addPack(ModPlatform::IndexedPack::Ptr pack, + ModPlatform::IndexedVersion& version, + const std::shared_ptr packs, + bool is_indexed = false, + QString custom_target_folder = {}); + void removePack(const QString& rem); + QList 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 m_failed_icon_actions; QList m_packs; + QList 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 f75bb886d..aab2ee89a 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 @@ -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(); + return m_model->data(m_ui->packView->currentIndex(), Qt::UserRole).value(); } 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 = "" + name + ""; + text = "websiteUrl + "\">" + name + ""; - 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("%2").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 += "
" + 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 += "

" + tr("Donate information: "); auto donateToStr = [](ModPlatform::DonationData& donate) -> QString { return QString("%2").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 += "

" + tr("External links:") + "
"; } - if (!current_pack.extraData.issuesUrl.isEmpty()) - text += "- " + tr("Issues: %1").arg(current_pack.extraData.issuesUrl) + "
"; - if (!current_pack.extraData.wikiUrl.isEmpty()) - text += "- " + tr("Wiki: %1").arg(current_pack.extraData.wikiUrl) + "
"; - if (!current_pack.extraData.sourceUrl.isEmpty()) - text += "- " + tr("Source code: %1").arg(current_pack.extraData.sourceUrl) + "
"; - if (!current_pack.extraData.discordUrl.isEmpty()) - text += "- " + tr("Discord: %1").arg(current_pack.extraData.discordUrl) + "
"; + if (!current_pack->extraData.issuesUrl.isEmpty()) + text += "- " + tr("Issues: %1").arg(current_pack->extraData.issuesUrl) + "
"; + if (!current_pack->extraData.wikiUrl.isEmpty()) + text += "- " + tr("Wiki: %1").arg(current_pack->extraData.wikiUrl) + "
"; + if (!current_pack->extraData.sourceUrl.isEmpty()) + text += "- " + tr("Source code: %1").arg(current_pack->extraData.sourceUrl) + "
"; + if (!current_pack->extraData.discordUrl.isEmpty()) + text += "- " + tr("Discord: %1").arg(current_pack->extraData.discordUrl) + "
"; } text += "
"; 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, version); + m_parent_dialog->removeResource(pack_name); +} + +void ResourcePage::addResourceToPage(ModPlatform::IndexedPack::Ptr pack, + ModPlatform::IndexedVersion& ver, + const std::shared_ptr base_model) +{ + m_model->addPack(pack, ver, base_model); +} + +void ResourcePage::removeResourceFromPage(const QString& name) +{ + 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 1896d53ea..b4a87f573 100644 --- a/launcher/ui/pages/modplatform/ResourcePage.h +++ b/launcher/ui/pages/modplatform/ResourcePage.h @@ -7,10 +7,12 @@ #include #include +#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; ~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); + + QList 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 251c07e71..fbf94e844 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 ShaderPackResourcePage::urlHandlers() const { QMap 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 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 9039c4d91..fcf6d4a7c 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) override; [[nodiscard]] QMap urlHandlers() const override; diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp index 9ad26f472..c6b087d67 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp @@ -16,62 +16,49 @@ #include "AtlListModel.h" -#include #include +#include #include namespace Atl { -ListModel::ListModel(QObject *parent) : QAbstractListModel(parent) -{ -} +ListModel::ListModel(QObject* parent) : QAbstractListModel(parent) {} -ListModel::~ListModel() -{ -} +ListModel::~ListModel() {} -int ListModel::rowCount(const QModelIndex &parent) const +int ListModel::rowCount(const QModelIndex& parent) const { return parent.isValid() ? 0 : modpacks.size(); } -int ListModel::columnCount(const QModelIndex &parent) const +int ListModel::columnCount(const QModelIndex& parent) const { return parent.isValid() ? 0 : 1; } -QVariant ListModel::data(const QModelIndex &index, int role) const +QVariant ListModel::data(const QModelIndex& index, int role) const { int pos = index.row(); - if(pos >= modpacks.size() || pos < 0 || !index.isValid()) - { + if (pos >= modpacks.size() || pos < 0 || !index.isValid()) { return QString("INVALID INDEX %1").arg(pos); } ATLauncher::IndexedPack pack = modpacks.at(pos); - if(role == Qt::DisplayRole) - { + if (role == Qt::DisplayRole) { return pack.name; - } - else if (role == Qt::ToolTipRole) - { + } else if (role == Qt::ToolTipRole) { return pack.name; - } - else if(role == Qt::DecorationRole) - { - if(m_logoMap.contains(pack.safeName)) - { + } else if (role == Qt::DecorationRole) { + if (m_logoMap.contains(pack.safeName)) { return (m_logoMap.value(pack.safeName)); } auto icon = APPLICATION->getThemedIcon("atlauncher-placeholder"); auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/images/%1.png").arg(pack.safeName.toLower()); - ((ListModel *)this)->requestLogo(pack.safeName, url); + ((ListModel*)this)->requestLogo(pack.safeName, url); return icon; - } - else if(role == Qt::UserRole) - { + } else if (role == Qt::UserRole) { QVariant v; v.setValue(pack); return v; @@ -88,7 +75,7 @@ void ListModel::request() auto netJob = makeShared("Atl::Request", APPLICATION->network()); auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/json/packsnew.json"); - netJob->addNetAction(Net::Download::makeByteArray(QUrl(url), &response)); + netJob->addNetAction(Net::Download::makeByteArray(QUrl(url), response)); jobPtr = netJob; jobPtr->start(); @@ -101,36 +88,38 @@ void ListModel::requestFinished() jobPtr.reset(); QJsonParseError parse_error; - QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); - if(parse_error.error != QJsonParseError::NoError) { + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from ATL at " << parse_error.offset << " reason: " << parse_error.errorString(); - qWarning() << response; + qWarning() << *response; return; } QList newList; auto packs = doc.array(); - for(auto packRaw : packs) { + for (auto packRaw : packs) { auto packObj = packRaw.toObject(); ATLauncher::IndexedPack pack; try { ATLauncher::loadIndexedPack(pack, packObj); - } - catch (const JSONValidationError &e) { - qDebug() << QString::fromUtf8(response); + } catch (const JSONValidationError& e) { + qDebug() << QString::fromUtf8(*response); qWarning() << "Error while reading pack manifest from ATLauncher: " << e.cause(); return; } // ignore packs without a published version - if(pack.versions.length() == 0) continue; + if (pack.versions.length() == 0) + continue; // only display public packs (for now) - if(pack.type != ATLauncher::PackType::Public) continue; + if (pack.type != ATLauncher::PackType::Public) + continue; // ignore "system" packs (Vanilla, Vanilla with Forge, etc) - if(pack.system) continue; + if (pack.system) + continue; newList.append(pack); } @@ -145,14 +134,12 @@ void ListModel::requestFailed(QString reason) jobPtr.reset(); } -void ListModel::getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback) +void ListModel::getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback) { - if(m_logoMap.contains(logo)) - { - callback(APPLICATION->metacache()->resolveEntry("ATLauncherPacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath()); - } - else - { + if (m_logoMap.contains(logo)) { + callback( + APPLICATION->metacache()->resolveEntry("ATLauncherPacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath()); + } else { requestLogo(logo, logoUrl); } } @@ -168,36 +155,34 @@ void ListModel::logoLoaded(QString logo, QIcon out) m_loadingLogos.removeAll(logo); m_logoMap.insert(logo, out); - for(int i = 0; i < modpacks.size(); i++) { - if(modpacks[i].safeName == logo) { - emit dataChanged(createIndex(i, 0), createIndex(i, 0), {Qt::DecorationRole}); + for (int i = 0; i < modpacks.size(); i++) { + if (modpacks[i].safeName == logo) { + emit dataChanged(createIndex(i, 0), createIndex(i, 0), { Qt::DecorationRole }); } } } void ListModel::requestLogo(QString file, QString url) { - if(m_loadingLogos.contains(file) || m_failedLogos.contains(file)) - { + if (m_loadingLogos.contains(file) || m_failedLogos.contains(file)) { return; } MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("ATLauncherPacks", QString("logos/%1").arg(file.section(".", 0, 0))); - NetJob *job = new NetJob(QString("ATLauncher Icon Download %1").arg(file), APPLICATION->network()); + auto job = new NetJob(QString("ATLauncher Icon Download %1").arg(file), APPLICATION->network()); job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); auto fullPath = entry->getFullPath(); - QObject::connect(job, &NetJob::succeeded, this, [this, file, fullPath] - { + QObject::connect(job, &NetJob::succeeded, this, [this, file, fullPath, job] { + job->deleteLater(); emit logoLoaded(file, QIcon(fullPath)); - if(waitingCallbacks.contains(file)) - { + if (waitingCallbacks.contains(file)) { waitingCallbacks.value(file)(fullPath); } }); - QObject::connect(job, &NetJob::failed, this, [this, file] - { + QObject::connect(job, &NetJob::failed, this, [this, file, job] { + job->deleteLater(); emit logoFailed(file); }); @@ -206,4 +191,4 @@ void ListModel::requestLogo(QString file, QString url) m_loadingLogos.append(file); } -} +} // namespace Atl diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.h b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.h index 2574c48d6..ed1fdc9f9 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.h +++ b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.h @@ -18,42 +18,41 @@ #include -#include "net/NetJob.h" -#include #include +#include +#include "net/NetJob.h" namespace Atl { typedef QMap LogoMap; typedef std::function LogoCallback; -class ListModel : public QAbstractListModel -{ +class ListModel : public QAbstractListModel { Q_OBJECT -public: - ListModel(QObject *parent); + public: + ListModel(QObject* parent); virtual ~ListModel(); - int rowCount(const QModelIndex &parent) const override; - int columnCount(const QModelIndex &parent) const override; - QVariant data(const QModelIndex &index, int role) const override; + int rowCount(const QModelIndex& parent) const override; + int columnCount(const QModelIndex& parent) const override; + QVariant data(const QModelIndex& index, int role) const override; void request(); - void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback); + void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback); -private slots: + private slots: void requestFinished(); void requestFailed(QString reason); void logoFailed(QString logo); void logoLoaded(QString logo, QIcon out); -private: + private: void requestLogo(QString file, QString url); -private: + private: QList modpacks; QStringList m_failedLogos; @@ -62,7 +61,7 @@ private: QMap waitingCallbacks; NetJob::Ptr jobPtr; - QByteArray response; + std::shared_ptr response = std::make_shared(); }; -} +} // namespace Atl diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp index cdb4532c8..7b61daa71 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp @@ -152,7 +152,7 @@ Qt::ItemFlags AtlOptionalModListModel::flags(const QModelIndex &index) const { void AtlOptionalModListModel::useShareCode(const QString& code) { m_jobPtr.reset(new NetJob("Atl::Request", APPLICATION->network())); auto url = QString(BuildConfig.ATL_API_BASE_URL + "share-codes/" + code); - m_jobPtr->addNetAction(Net::Download::makeByteArray(QUrl(url), &m_response)); + m_jobPtr->addNetAction(Net::Download::makeByteArray(QUrl(url), m_response)); connect(m_jobPtr.get(), &NetJob::succeeded, this, &AtlOptionalModListModel::shareCodeSuccess); @@ -166,10 +166,10 @@ void AtlOptionalModListModel::shareCodeSuccess() { m_jobPtr.reset(); QJsonParseError parse_error {}; - auto doc = QJsonDocument::fromJson(m_response, &parse_error); + auto doc = QJsonDocument::fromJson(*m_response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from ATL at " << parse_error.offset << " reason: " << parse_error.errorString(); - qWarning() << m_response; + qWarning() << *m_response; return; } auto obj = doc.object(); @@ -179,7 +179,7 @@ void AtlOptionalModListModel::shareCodeSuccess() { ATLauncher::loadShareCodeResponse(response, obj); } catch (const JSONValidationError& e) { - qDebug() << QString::fromUtf8(m_response); + qDebug() << QString::fromUtf8(*m_response); qWarning() << "Error while reading response from ATLauncher: " << e.cause(); return; } diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h index 8e02444e4..639f0d489 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h +++ b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h @@ -82,9 +82,9 @@ private: void toggleMod(ATLauncher::VersionMod mod, int index); void setMod(ATLauncher::VersionMod mod, int index, bool enable, bool shouldEmit = true); -private: + private: NetJob::Ptr m_jobPtr; - QByteArray m_response; + std::shared_ptr m_response = std::make_shared(); ATLauncher::PackVersion m_version; QVector m_mods; diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.cpp index f5f50caee..3d2d568a1 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.cpp @@ -68,7 +68,7 @@ QString AtlUserInteractionSupportImpl::chooseVersion(Meta::VersionList::Ptr vlis // select recommended build for (int i = 0; i < vlist->versions().size(); i++) { auto version = vlist->versions().at(i); - auto reqs = version->requires(); + auto reqs = version->requiredSet(); // filter by minecraft version, if the loader depends on a certain version. if (minecraftVersion != nullptr) { diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.h b/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.h index 37010b3ff..adeb53cbc 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.h +++ b/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.h @@ -42,15 +42,15 @@ class AtlUserInteractionSupportImpl : public QObject, public ATLauncher::UserInteractionSupport { Q_OBJECT -public: + public: AtlUserInteractionSupportImpl(QWidget* parent); + virtual ~AtlUserInteractionSupportImpl() = default; -private: + private: QString chooseVersion(Meta::VersionList::Ptr vlist, QString minecraftVersion) override; std::optional> chooseOptionalMods(ATLauncher::PackVersion version, QVector mods) override; void displayMessage(QString message) override; -private: + private: QWidget* m_parent; - }; diff --git a/launcher/ui/pages/modplatform/flame/FlameModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModel.cpp index 5961ea026..fa55aa686 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModel.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModel.cpp @@ -60,6 +60,8 @@ QVariant ListModel::data(const QModelIndex& index, int role) const return pack.description; case UserDataTypes::SELECTED: return false; + case UserDataTypes::INSTALLED: + return false; default: break; } @@ -169,7 +171,7 @@ void ListModel::performPaginatedSearch() .arg(currentSearchTerm) .arg(currentSort + 1); - netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); + netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), response)); jobPtr = netJob; jobPtr->start(); QObject::connect(netJob.get(), &NetJob::succeeded, this, &ListModel::searchRequestFinished); @@ -202,11 +204,11 @@ void Flame::ListModel::searchRequestFinished() jobPtr.reset(); QJsonParseError parse_error; - QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from CurseForge at " << parse_error.offset << " reason: " << parse_error.errorString(); - qWarning() << response; + qWarning() << *response; return; } diff --git a/launcher/ui/pages/modplatform/flame/FlameModel.h b/launcher/ui/pages/modplatform/flame/FlameModel.h index cab666cc3..b3bc96b8c 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModel.h +++ b/launcher/ui/pages/modplatform/flame/FlameModel.h @@ -3,46 +3,44 @@ #include #include -#include -#include #include -#include #include +#include +#include #include #include -#include +#include +#include -#include #include +#include #include namespace Flame { - typedef QMap LogoMap; typedef std::function LogoCallback; -class ListModel : public QAbstractListModel -{ +class ListModel : public QAbstractListModel { Q_OBJECT -public: - ListModel(QObject *parent); + public: + ListModel(QObject* parent); virtual ~ListModel(); - int rowCount(const QModelIndex &parent) const override; - int columnCount(const QModelIndex &parent) const override; - QVariant data(const QModelIndex &index, int role) const override; - bool setData(const QModelIndex &index, const QVariant &value, int role) override; - Qt::ItemFlags flags(const QModelIndex &index) const override; - bool canFetchMore(const QModelIndex & parent) const override; - void fetchMore(const QModelIndex & parent) override; + int rowCount(const QModelIndex& parent) const override; + int columnCount(const QModelIndex& parent) const override; + QVariant data(const QModelIndex& index, int role) const override; + bool setData(const QModelIndex& index, const QVariant& value, int role) override; + Qt::ItemFlags flags(const QModelIndex& index) const override; + bool canFetchMore(const QModelIndex& parent) const override; + void fetchMore(const QModelIndex& parent) override; - void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback); - void searchWithTerm(const QString & term, const int sort); + void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback); + void searchWithTerm(const QString& term, const int sort); -private slots: + private slots: void performPaginatedSearch(); void logoFailed(QString logo); @@ -51,10 +49,10 @@ private slots: void searchRequestFinished(); void searchRequestFailed(QString reason); -private: + private: void requestLogo(QString file, QString url); -private: + private: QList modpacks; QStringList m_failedLogos; QStringList m_loadingLogos; @@ -64,14 +62,9 @@ private: QString currentSearchTerm; int currentSort = 0; int nextSearchOffset = 0; - enum SearchState { - None, - CanPossiblyFetchMore, - ResetRequested, - Finished - } searchState = None; + enum SearchState { None, CanPossiblyFetchMore, ResetRequested, Finished } searchState = None; NetJob::Ptr jobPtr; - QByteArray response; + std::shared_ptr response = std::make_shared(); }; -} +} // namespace Flame diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.cpp b/launcher/ui/pages/modplatform/flame/FlamePage.cpp index f9ac4a789..cef26bb6b 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlamePage.cpp @@ -130,7 +130,7 @@ void FlamePage::onSelectionChanged(QModelIndex curr, QModelIndex prev) if (current.versionsLoaded == false) { qDebug() << "Loading flame modpack versions"; auto netJob = new NetJob(QString("Flame::PackVersions(%1)").arg(current.name), APPLICATION->network()); - auto response = new QByteArray(); + auto response = std::make_shared(); int addonId = current.addonId; netJob->addNetAction(Net::Download::makeByteArray(QString("https://api.curseforge.com/v1/mods/%1/files").arg(addonId), response)); @@ -170,10 +170,7 @@ void FlamePage::onSelectionChanged(QModelIndex curr, QModelIndex prev) } suggestCurrent(); }); - QObject::connect(netJob, &NetJob::finished, this, [response, netJob] { - netJob->deleteLater(); - delete response; - }); + QObject::connect(netJob, &NetJob::finished, this, [response, netJob] { netJob->deleteLater(); }); netJob->start(); } else { for (auto version : current.versions) { diff --git a/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp b/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp index e3d0bc144..0fb67c507 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp @@ -11,7 +11,7 @@ namespace ResourceDownload { -FlameModModel::FlameModModel(BaseInstance const& base) : ModModel(base, new FlameAPI) {} +FlameModModel::FlameModModel(BaseInstance& base) : ModModel(base, new FlameAPI) {} void FlameModModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) { @@ -29,6 +29,11 @@ void FlameModModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonAr FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance); } +auto FlameModModel::loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion +{ + return FlameMod::loadDependencyVersions(m, arr); +}; + auto FlameModModel::documentToArray(QJsonDocument& obj) const -> QJsonArray { return Json::ensureArray(obj.object(), "data"); @@ -81,7 +86,7 @@ void FlameTexturePackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, auto const& mc_versions = version.mcVersion; if (std::any_of(mc_versions.constBegin(), mc_versions.constEnd(), - [this](auto const& mc_version){ return Version(mc_version) <= maximumTexturePackVersion(); })) + [this](auto const& mc_version) { return Version(mc_version) <= maximumTexturePackVersion(); })) filtered_versions.push_back(version); } diff --git a/launcher/ui/pages/modplatform/flame/FlameResourceModels.h b/launcher/ui/pages/modplatform/flame/FlameResourceModels.h index 0252ac403..6cfd6a6f8 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourceModels.h +++ b/launcher/ui/pages/modplatform/flame/FlameResourceModels.h @@ -14,7 +14,7 @@ class FlameModModel : public ModModel { Q_OBJECT public: - FlameModModel(const BaseInstance&); + FlameModModel(BaseInstance&); ~FlameModModel() override = default; private: @@ -24,6 +24,7 @@ class FlameModModel : public ModModel { void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override; void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; + auto loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion override; auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; }; diff --git a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp index 2343b79f2..330dd4fb8 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp +++ b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp @@ -38,11 +38,11 @@ #include "net/HttpMetaCache.h" #include "net/NetJob.h" -#include "StringUtils.h" #include +#include "StringUtils.h" -#include #include +#include #include @@ -50,33 +50,33 @@ namespace LegacyFTB { -FilterModel::FilterModel(QObject *parent) : QSortFilterProxyModel(parent) +FilterModel::FilterModel(QObject* parent) : QSortFilterProxyModel(parent) { currentSorting = Sorting::ByGameVersion; sortings.insert(tr("Sort by Name"), Sorting::ByName); sortings.insert(tr("Sort by Game Version"), Sorting::ByGameVersion); } -bool FilterModel::lessThan(const QModelIndex &left, const QModelIndex &right) const +bool FilterModel::lessThan(const QModelIndex& left, const QModelIndex& right) const { Modpack leftPack = sourceModel()->data(left, Qt::UserRole).value(); Modpack rightPack = sourceModel()->data(right, Qt::UserRole).value(); - if(currentSorting == Sorting::ByGameVersion) { + if (currentSorting == Sorting::ByGameVersion) { Version lv(leftPack.mcVersion); Version rv(rightPack.mcVersion); return lv < rv; - } else if(currentSorting == Sorting::ByName) { + } else if (currentSorting == Sorting::ByName) { return StringUtils::naturalCompare(leftPack.name, rightPack.name, Qt::CaseSensitive) >= 0; } - //UHM, some inavlid value set?! + // UHM, some inavlid value set?! qWarning() << "Invalid sorting set!"; return true; } -bool FilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const +bool FilterModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const { return true; } @@ -102,18 +102,13 @@ FilterModel::Sorting FilterModel::getCurrentSorting() return currentSorting; } -ListModel::ListModel(QObject *parent) : QAbstractListModel(parent) -{ -} +ListModel::ListModel(QObject* parent) : QAbstractListModel(parent) {} -ListModel::~ListModel() -{ -} +ListModel::~ListModel() {} QString ListModel::translatePackType(PackType type) const { - switch(type) - { + switch (type) { case PackType::Public: return tr("Public Modpack"); case PackType::ThirdParty: @@ -125,67 +120,51 @@ QString ListModel::translatePackType(PackType type) const return QString(); } -int ListModel::rowCount(const QModelIndex &parent) const +int ListModel::rowCount(const QModelIndex& parent) const { return parent.isValid() ? 0 : modpacks.size(); } -int ListModel::columnCount(const QModelIndex &parent) const +int ListModel::columnCount(const QModelIndex& parent) const { return parent.isValid() ? 0 : 1; } -QVariant ListModel::data(const QModelIndex &index, int role) const +QVariant ListModel::data(const QModelIndex& index, int role) const { int pos = index.row(); - if(pos >= modpacks.size() || pos < 0 || !index.isValid()) - { + if (pos >= modpacks.size() || pos < 0 || !index.isValid()) { return QString("INVALID INDEX %1").arg(pos); } Modpack pack = modpacks.at(pos); - if(role == Qt::DisplayRole) - { + if (role == Qt::DisplayRole) { return pack.name + "\n" + translatePackType(pack.type); - } - else if (role == Qt::ToolTipRole) - { - if(pack.description.length() > 100) - { - //some magic to prevent to long tooltips and replace html linebreaks + } else if (role == Qt::ToolTipRole) { + if (pack.description.length() > 100) { + // some magic to prevent to long tooltips and replace html linebreaks QString edit = pack.description.left(97); edit = edit.left(edit.lastIndexOf("
")).left(edit.lastIndexOf(" ")).append("..."); return edit; - } return pack.description; - } - else if(role == Qt::DecorationRole) - { - if(m_logoMap.contains(pack.logo)) - { + } else if (role == Qt::DecorationRole) { + if (m_logoMap.contains(pack.logo)) { return (m_logoMap.value(pack.logo)); } QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); - ((ListModel *)this)->requestLogo(pack.logo); + ((ListModel*)this)->requestLogo(pack.logo); return icon; - } - else if(role == Qt::ForegroundRole) - { - if(pack.broken) - { - //FIXME: Hardcoded color + } else if (role == Qt::ForegroundRole) { + if (pack.broken) { + // FIXME: Hardcoded color return QColor(255, 0, 50); - } - else if(pack.bugged) - { - //FIXME: Hardcoded color - //bugged pack, currently only indicates bugged xml + } else if (pack.bugged) { + // FIXME: Hardcoded color + // bugged pack, currently only indicates bugged xml return QColor(244, 229, 66); } - } - else if(role == Qt::UserRole) - { + } else if (role == Qt::UserRole) { QVariant v; v.setValue(pack); return v; @@ -222,8 +201,7 @@ Modpack ListModel::at(int row) void ListModel::remove(int row) { - if(row < 0 || row >= modpacks.size()) - { + if (row < 0 || row >= modpacks.size()) { qWarning() << "Attempt to remove FTB modpacks with invalid row" << row; return; } @@ -247,27 +225,25 @@ void ListModel::logoFailed(QString logo) void ListModel::requestLogo(QString file) { - if(m_loadingLogos.contains(file) || m_failedLogos.contains(file)) - { + if (m_loadingLogos.contains(file) || m_failedLogos.contains(file)) { return; } MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("FTBPacks", QString("logos/%1").arg(file.section(".", 0, 0))); - NetJob *job = new NetJob(QString("FTB Icon Download for %1").arg(file), APPLICATION->network()); + NetJob* job = new NetJob(QString("FTB Icon Download for %1").arg(file), APPLICATION->network()); job->addNetAction(Net::Download::makeCached(QUrl(QString(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/%1").arg(file)), entry)); auto fullPath = entry->getFullPath(); - QObject::connect(job, &NetJob::finished, this, [this, file, fullPath] - { + QObject::connect(job, &NetJob::finished, this, [this, file, fullPath, job] { + job->deleteLater(); emit logoLoaded(file, QIcon(fullPath)); - if(waitingCallbacks.contains(file)) - { + if (waitingCallbacks.contains(file)) { waitingCallbacks.value(file)(fullPath); } }); - QObject::connect(job, &NetJob::failed, this, [this, file] - { + QObject::connect(job, &NetJob::failed, this, [this, file, job] { + job->deleteLater(); emit logoFailed(file); }); @@ -276,21 +252,18 @@ void ListModel::requestLogo(QString file) m_loadingLogos.append(file); } -void ListModel::getLogo(const QString &logo, LogoCallback callback) +void ListModel::getLogo(const QString& logo, LogoCallback callback) { - if(m_logoMap.contains(logo)) - { + if (m_logoMap.contains(logo)) { callback(APPLICATION->metacache()->resolveEntry("FTBPacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath()); - } - else - { + } else { requestLogo(logo); } } -Qt::ItemFlags ListModel::flags(const QModelIndex &index) const +Qt::ItemFlags ListModel::flags(const QModelIndex& index) const { return QAbstractListModel::flags(index); } -} +} // namespace LegacyFTB diff --git a/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp b/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp index 98ab87999..b3f6261fe 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp +++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp @@ -116,8 +116,8 @@ Page::Page(NewInstanceDialog* dialog, QWidget *parent) connect(ui->thirdPartyPackList->selectionModel(), &QItemSelectionModel::currentChanged, this, &Page::onThirdPartyPackSelectionChanged); connect(ui->privatePackList->selectionModel(), &QItemSelectionModel::currentChanged, this, &Page::onPrivatePackSelectionChanged); - connect(ui->addPackBtn, &QPushButton::pressed, this, &Page::onAddPackClicked); - connect(ui->removePackBtn, &QPushButton::pressed, this, &Page::onRemovePackClicked); + connect(ui->addPackBtn, &QPushButton::clicked, this, &Page::onAddPackClicked); + connect(ui->removePackBtn, &QPushButton::clicked, this, &Page::onRemovePackClicked); connect(ui->tabWidget, &QTabWidget::currentChanged, this, &Page::onTabChanged); diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index 346a00b0e..e0046d887 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -106,6 +106,8 @@ auto ModpackListModel::data(const QModelIndex& index, int role) const -> QVarian return pack.description; case UserDataTypes::SELECTED: return false; + case UserDataTypes::INSTALLED: + return false; default: break; } @@ -129,27 +131,27 @@ void ModpackListModel::performPaginatedSearch() // TODO: Move to standalone API auto netJob = makeShared("Modrinth::SearchModpack", APPLICATION->network()); auto searchAllUrl = QString(BuildConfig.MODRINTH_PROD_URL + - "/search?" - "offset=%1&" - "limit=%2&" - "query=%3&" - "index=%4&" - "facets=[[\"project_type:modpack\"]]") + "/search?" + "offset=%1&" + "limit=%2&" + "query=%3&" + "index=%4&" + "facets=[[\"project_type:modpack\"]]") .arg(nextSearchOffset) .arg(m_modpacks_per_page) .arg(currentSearchTerm) .arg(currentSort); - netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchAllUrl), &m_all_response)); + netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchAllUrl), m_all_response)); QObject::connect(netJob.get(), &NetJob::succeeded, this, [this] { QJsonParseError parse_error_all{}; - QJsonDocument doc_all = QJsonDocument::fromJson(m_all_response, &parse_error_all); + QJsonDocument doc_all = QJsonDocument::fromJson(*m_all_response, &parse_error_all); if (parse_error_all.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from " << debugName() << " at " << parse_error_all.offset << " reason: " << parse_error_all.errorString(); - qWarning() << m_all_response; + qWarning() << *m_all_response; return; } diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h index 6e6be4b9e..b9e9c3da5 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h @@ -110,9 +110,9 @@ class ModpackListModel : public QAbstractListModel { NetJob::Ptr jobPtr; - QByteArray m_all_response; + std::shared_ptr m_all_response = std::make_shared(); QByteArray m_specific_response; int m_modpacks_per_page = 20; }; -} // namespace ModPlatform +} // namespace Modrinth diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index 0bb11d834..c71dd9038 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -123,7 +123,7 @@ void ModrinthPage::onSelectionChanged(QModelIndex curr, QModelIndex prev) qDebug() << "Loading modrinth modpack information"; auto netJob = new NetJob(QString("Modrinth::PackInformation(%1)").arg(current.name), APPLICATION->network()); - auto response = new QByteArray(); + auto response = std::make_shared(); QString id = current.id; @@ -162,10 +162,7 @@ void ModrinthPage::onSelectionChanged(QModelIndex curr, QModelIndex prev) suggestCurrent(); }); - QObject::connect(netJob, &NetJob::finished, this, [response, netJob] { - netJob->deleteLater(); - delete response; - }); + QObject::connect(netJob, &NetJob::finished, this, [response, netJob] { netJob->deleteLater(); }); netJob->start(); } else updateUI(); @@ -174,7 +171,7 @@ void ModrinthPage::onSelectionChanged(QModelIndex curr, QModelIndex prev) qDebug() << "Loading modrinth modpack versions"; auto netJob = new NetJob(QString("Modrinth::PackVersions(%1)").arg(current.name), APPLICATION->network()); - auto response = new QByteArray(); + auto response = std::make_shared(); QString id = current.id; @@ -217,10 +214,7 @@ void ModrinthPage::onSelectionChanged(QModelIndex curr, QModelIndex prev) suggestCurrent(); }); - QObject::connect(netJob, &NetJob::finished, this, [response, netJob] { - netJob->deleteLater(); - delete response; - }); + QObject::connect(netJob, &NetJob::finished, this, [response, netJob] { netJob->deleteLater(); }); netJob->start(); } else { @@ -260,10 +254,8 @@ void ModrinthPage::updateUI() text += donates.join(", "); } - if (!current.extra.issuesUrl.isEmpty() - || !current.extra.sourceUrl.isEmpty() - || !current.extra.wikiUrl.isEmpty() - || !current.extra.discordUrl.isEmpty()) { + if (!current.extra.issuesUrl.isEmpty() || !current.extra.sourceUrl.isEmpty() || !current.extra.wikiUrl.isEmpty() || + !current.extra.discordUrl.isEmpty()) { text += "

" + tr("External links:") + "
"; } diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp index f5d1cc282..8aa649898 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp @@ -25,7 +25,7 @@ namespace ResourceDownload { -ModrinthModModel::ModrinthModModel(BaseInstance const& base) : ModModel(base, new ModrinthAPI) {} +ModrinthModModel::ModrinthModModel(BaseInstance& base) : ModModel(base, new ModrinthAPI) {} void ModrinthModModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) { @@ -42,12 +42,17 @@ void ModrinthModModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJso ::Modrinth::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance); } +auto ModrinthModModel::loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion +{ + return ::Modrinth::loadDependencyVersions(m, arr); +}; + auto ModrinthModModel::documentToArray(QJsonDocument& obj) const -> QJsonArray { return obj.object().value("hits").toArray(); } -ModrinthResourcePackModel::ModrinthResourcePackModel(const BaseInstance& base) : ResourcePackResourceModel(base, new ModrinthAPI){} +ModrinthResourcePackModel::ModrinthResourcePackModel(const BaseInstance& base) : ResourcePackResourceModel(base, new ModrinthAPI) {} void ModrinthResourcePackModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) { @@ -69,7 +74,7 @@ auto ModrinthResourcePackModel::documentToArray(QJsonDocument& obj) const -> QJs return obj.object().value("hits").toArray(); } -ModrinthTexturePackModel::ModrinthTexturePackModel(const BaseInstance& base) : TexturePackResourceModel(base, new ModrinthAPI){} +ModrinthTexturePackModel::ModrinthTexturePackModel(const BaseInstance& base) : TexturePackResourceModel(base, new ModrinthAPI) {} void ModrinthTexturePackModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) { @@ -91,7 +96,7 @@ auto ModrinthTexturePackModel::documentToArray(QJsonDocument& obj) const -> QJso return obj.object().value("hits").toArray(); } -ModrinthShaderPackModel::ModrinthShaderPackModel(const BaseInstance& base) : ShaderPackResourceModel(base, new ModrinthAPI){} +ModrinthShaderPackModel::ModrinthShaderPackModel(const BaseInstance& base) : ShaderPackResourceModel(base, new ModrinthAPI) {} void ModrinthShaderPackModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) { diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h index b351b19b9..d7c858f88 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h @@ -30,7 +30,7 @@ class ModrinthModModel : public ModModel { Q_OBJECT public: - ModrinthModModel(const BaseInstance&); + ModrinthModModel(BaseInstance&); ~ModrinthModModel() override = default; private: @@ -40,6 +40,7 @@ class ModrinthModModel : public ModModel { void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override; void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; + auto loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion override; auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; }; diff --git a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp index 50f0c72d1..f08eb2897 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp @@ -40,39 +40,28 @@ #include -Technic::ListModel::ListModel(QObject *parent) : QAbstractListModel(parent) -{ -} +Technic::ListModel::ListModel(QObject* parent) : QAbstractListModel(parent) {} -Technic::ListModel::~ListModel() -{ -} +Technic::ListModel::~ListModel() {} QVariant Technic::ListModel::data(const QModelIndex& index, int role) const { int pos = index.row(); - if(pos >= modpacks.size() || pos < 0 || !index.isValid()) - { + if (pos >= modpacks.size() || pos < 0 || !index.isValid()) { return QString("INVALID INDEX %1").arg(pos); } Modpack pack = modpacks.at(pos); - if(role == Qt::DisplayRole) - { + if (role == Qt::DisplayRole) { return pack.name; - } - else if(role == Qt::DecorationRole) - { - if(m_logoMap.contains(pack.logoName)) - { + } else if (role == Qt::DecorationRole) { + if (m_logoMap.contains(pack.logoName)) { return (m_logoMap.value(pack.logoName)); } QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); - ((ListModel *)this)->requestLogo(pack.logoName, pack.logoUrl); + ((ListModel*)this)->requestLogo(pack.logoName, pack.logoUrl); return icon; - } - else if(role == Qt::UserRole) - { + } else if (role == Qt::UserRole) { QVariant v; v.setValue(pack); return v; @@ -92,16 +81,15 @@ int Technic::ListModel::rowCount(const QModelIndex& parent) const void Technic::ListModel::searchWithTerm(const QString& term) { - if(currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull()) { + if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull()) { return; } currentSearchTerm = term; - if(jobPtr) { + if (jobPtr) { jobPtr->abort(); searchState = ResetRequested; return; - } - else { + } else { beginResetModel(); modpacks.clear(); endResetModel(); @@ -115,26 +103,20 @@ void Technic::ListModel::performSearch() auto netJob = makeShared("Technic::Search", APPLICATION->network()); QString searchUrl = ""; if (currentSearchTerm.isEmpty()) { - searchUrl = QString("%1trending?build=%2") - .arg(BuildConfig.TECHNIC_API_BASE_URL, BuildConfig.TECHNIC_API_BUILD); + searchUrl = QString("%1trending?build=%2").arg(BuildConfig.TECHNIC_API_BASE_URL, BuildConfig.TECHNIC_API_BUILD); searchMode = List; - } - else if (currentSearchTerm.startsWith("http://api.technicpack.net/modpack/")) { - searchUrl = QString("https://%1?build=%2") - .arg(currentSearchTerm.mid(7), BuildConfig.TECHNIC_API_BUILD); + } else if (currentSearchTerm.startsWith("http://api.technicpack.net/modpack/")) { + searchUrl = QString("https://%1?build=%2").arg(currentSearchTerm.mid(7), BuildConfig.TECHNIC_API_BUILD); searchMode = Single; - } - else if (currentSearchTerm.startsWith("https://api.technicpack.net/modpack/")) { + } else if (currentSearchTerm.startsWith("https://api.technicpack.net/modpack/")) { searchUrl = QString("%1?build=%2").arg(currentSearchTerm, BuildConfig.TECHNIC_API_BUILD); searchMode = Single; - } - else { - searchUrl = QString( - "%1search?build=%2&q=%3" - ).arg(BuildConfig.TECHNIC_API_BASE_URL, BuildConfig.TECHNIC_API_BUILD, currentSearchTerm); + } else { + searchUrl = + QString("%1search?build=%2&q=%3").arg(BuildConfig.TECHNIC_API_BASE_URL, BuildConfig.TECHNIC_API_BUILD, currentSearchTerm); searchMode = List; } - netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); + netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), response)); jobPtr = netJob; jobPtr->start(); QObject::connect(netJob.get(), &NetJob::succeeded, this, &ListModel::searchRequestFinished); @@ -146,11 +128,11 @@ void Technic::ListModel::searchRequestFinished() jobPtr.reset(); QJsonParseError parse_error; - QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); - if(parse_error.error != QJsonParseError::NoError) - { - qWarning() << "Error while parsing JSON response from Technic at " << parse_error.offset << " reason: " << parse_error.errorString(); - qWarning() << response; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from Technic at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; return; } @@ -161,7 +143,7 @@ void Technic::ListModel::searchRequestFinished() switch (searchMode) { case List: { auto objs = Json::requireArray(root, "modpacks"); - for (auto technicPack: objs) { + for (auto technicPack : objs) { Modpack pack; auto technicPackObject = Json::requireObject(technicPack); pack.name = Json::requireString(technicPackObject, "name"); @@ -170,11 +152,10 @@ void Technic::ListModel::searchRequestFinished() continue; auto rawURL = Json::ensureString(technicPackObject, "iconUrl", "null"); - if(rawURL == "null") { + if (rawURL == "null") { pack.logoUrl = "null"; pack.logoName = "null"; - } - else { + } else { pack.logoUrl = rawURL; pack.logoName = rawURL.section(QLatin1Char('/'), -1).section(QLatin1Char('.'), 0, 0); } @@ -199,8 +180,7 @@ void Technic::ListModel::searchRequestFinished() pack.logoUrl = iconUrl; pack.logoName = iconUrl.section(QLatin1Char('/'), -1).section(QLatin1Char('.'), 0, 0); - } - else { + } else { pack.logoUrl = "null"; pack.logoName = "null"; } @@ -210,10 +190,8 @@ void Technic::ListModel::searchRequestFinished() break; } } - } - catch (const JSONValidationError &err) - { - qCritical() << "Couldn't parse technic search results:" << err.cause() ; + } catch (const JSONValidationError& err) { + qCritical() << "Couldn't parse technic search results:" << err.cause(); return; } searchState = Finished; @@ -229,12 +207,9 @@ void Technic::ListModel::searchRequestFinished() void Technic::ListModel::getLogo(const QString& logo, const QString& logoUrl, Technic::LogoCallback callback) { - if(m_logoMap.contains(logo)) - { + if (m_logoMap.contains(logo)) { callback(APPLICATION->metacache()->resolveEntry("TechnicPacks", QString("logos/%1").arg(logo))->getFullPath()); - } - else - { + } else { requestLogo(logo, logoUrl); } } @@ -243,30 +218,24 @@ void Technic::ListModel::searchRequestFailed() { jobPtr.reset(); - if(searchState == ResetRequested) - { + if (searchState == ResetRequested) { beginResetModel(); modpacks.clear(); endResetModel(); performSearch(); - } - else - { + } else { searchState = Finished; } } - void Technic::ListModel::logoLoaded(QString logo, QString out) { m_loadingLogos.removeAll(logo); m_logoMap.insert(logo, QIcon(out)); - for(int i = 0; i < modpacks.size(); i++) - { - if(modpacks[i].logoName == logo) - { - emit dataChanged(createIndex(i, 0), createIndex(i, 0), {Qt::DecorationRole}); + for (int i = 0; i < modpacks.size(); i++) { + if (modpacks[i].logoName == logo) { + emit dataChanged(createIndex(i, 0), createIndex(i, 0), { Qt::DecorationRole }); } } } @@ -279,24 +248,23 @@ void Technic::ListModel::logoFailed(QString logo) void Technic::ListModel::requestLogo(QString logo, QString url) { - if(m_loadingLogos.contains(logo) || m_failedLogos.contains(logo) || logo == "null") - { + if (m_loadingLogos.contains(logo) || m_failedLogos.contains(logo) || logo == "null") { return; } MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("TechnicPacks", QString("logos/%1").arg(logo)); - NetJob *job = new NetJob(QString("Technic Icon Download %1").arg(logo), APPLICATION->network()); + auto job = new NetJob(QString("Technic Icon Download %1").arg(logo), APPLICATION->network()); job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); auto fullPath = entry->getFullPath(); - QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath] - { + QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] { + job->deleteLater(); logoLoaded(logo, fullPath); }); - QObject::connect(job, &NetJob::failed, this, [this, logo] - { + QObject::connect(job, &NetJob::failed, this, [this, logo, job] { + job->deleteLater(); logoFailed(logo); }); diff --git a/launcher/ui/pages/modplatform/technic/TechnicModel.h b/launcher/ui/pages/modplatform/technic/TechnicModel.h index 5eea124cc..0f1a814e2 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicModel.h +++ b/launcher/ui/pages/modplatform/technic/TechnicModel.h @@ -44,33 +44,32 @@ namespace Technic { typedef std::function LogoCallback; -class ListModel : public QAbstractListModel -{ +class ListModel : public QAbstractListModel { Q_OBJECT -public: - ListModel(QObject *parent); + public: + ListModel(QObject* parent); virtual ~ListModel(); virtual QVariant data(const QModelIndex& index, int role) const; virtual int columnCount(const QModelIndex& parent) const; virtual int rowCount(const QModelIndex& parent) const; - void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback); - void searchWithTerm(const QString & term); + void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback); + void searchWithTerm(const QString& term); -private slots: + private slots: void searchRequestFinished(); void searchRequestFailed(); void logoFailed(QString logo); void logoLoaded(QString logo, QString out); -private: + private: void performSearch(); void requestLogo(QString logo, QString url); -private: + private: QList modpacks; QStringList m_failedLogos; QStringList m_loadingLogos; @@ -78,17 +77,13 @@ private: QMap waitingCallbacks; QString currentSearchTerm; - enum SearchState { - None, - ResetRequested, - Finished - } searchState = None; + enum SearchState { None, ResetRequested, Finished } searchState = None; enum SearchMode { List, Single, } searchMode = List; NetJob::Ptr jobPtr; - QByteArray response; + std::shared_ptr response = std::make_shared(); }; -} +} // namespace Technic diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp index 859da97e9..fc678fa20 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp @@ -143,7 +143,7 @@ void TechnicPage::suggestCurrent() auto netJob = makeShared(QString("Technic::PackMeta(%1)").arg(current.name), APPLICATION->network()); QString slug = current.slug; - netJob->addNetAction(Net::Download::makeByteArray(QString("%1modpack/%2?build=%3").arg(BuildConfig.TECHNIC_API_BASE_URL, slug, BuildConfig.TECHNIC_API_BUILD), &response)); + netJob->addNetAction(Net::Download::makeByteArray(QString("%1modpack/%2?build=%3").arg(BuildConfig.TECHNIC_API_BASE_URL, slug, BuildConfig.TECHNIC_API_BUILD), response)); QObject::connect(netJob.get(), &NetJob::succeeded, this, [this, slug] { jobPtr.reset(); @@ -154,7 +154,7 @@ void TechnicPage::suggestCurrent() } QJsonParseError parse_error {}; - QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); QJsonObject obj = doc.object(); if(parse_error.error != QJsonParseError::NoError) { @@ -249,7 +249,7 @@ void TechnicPage::metadataLoaded() auto netJob = makeShared(QString("Technic::SolderMeta(%1)").arg(current.name), APPLICATION->network()); auto url = QString("%1/modpack/%2").arg(current.url, current.slug); - netJob->addNetAction(Net::Download::makeByteArray(QUrl(url), &response)); + netJob->addNetAction(Net::Download::makeByteArray(QUrl(url), response)); QObject::connect(netJob.get(), &NetJob::succeeded, this, &TechnicPage::onSolderLoaded); @@ -291,11 +291,11 @@ void TechnicPage::onSolderLoaded() { current.versions.clear(); - QJsonParseError parse_error {}; - auto doc = QJsonDocument::fromJson(response, &parse_error); + QJsonParseError parse_error{}; + auto doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from Solder at " << parse_error.offset << " reason: " << parse_error.errorString(); - qWarning() << response; + qWarning() << *response; fallback(); return; } @@ -304,8 +304,7 @@ void TechnicPage::onSolderLoaded() { TechnicSolder::Pack pack; try { TechnicSolder::loadPack(pack, obj); - } - catch (const JSONValidationError& err) { + } catch (const JSONValidationError& err) { qCritical() << "Couldn't parse Solder pack metadata:" << err.cause(); fallback(); return; diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.h b/launcher/ui/pages/modplatform/technic/TechnicPage.h index f4a3b61d1..753261b30 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.h +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.h @@ -104,5 +104,5 @@ private: QString selectedVersion; NetJob::Ptr jobPtr; - QByteArray response; + std::shared_ptr response = std::make_shared(); }; diff --git a/launcher/ui/setupwizard/SetupWizard.cpp b/launcher/ui/setupwizard/SetupWizard.cpp index 3fd9bb233..0a47334fc 100644 --- a/launcher/ui/setupwizard/SetupWizard.cpp +++ b/launcher/ui/setupwizard/SetupWizard.cpp @@ -59,7 +59,7 @@ void SetupWizard::pageChanged(int id) { setButtonLayout({QWizard::CustomButton1, QWizard::Stretch, QWizard::BackButton, QWizard::NextButton, QWizard::FinishButton}); auto customButton = button(QWizard::CustomButton1); - connect(customButton, &QAbstractButton::pressed, [&](){ + connect(customButton, &QAbstractButton::clicked, [&](){ auto basePagePtr = getCurrentBasePage(); if(basePagePtr) { diff --git a/launcher/ui/themes/SystemTheme.cpp b/launcher/ui/themes/SystemTheme.cpp index a95bc8752..3a746d027 100644 --- a/launcher/ui/themes/SystemTheme.cpp +++ b/launcher/ui/themes/SystemTheme.cpp @@ -43,7 +43,7 @@ SystemTheme::SystemTheme() { themeDebugLog() << "Determining System Theme..."; const auto& style = QApplication::style(); - systemPalette = style->standardPalette(); + systemPalette = QApplication::palette(); QString lowerThemeName = style->objectName(); themeDebugLog() << "System theme seems to be:" << lowerThemeName; QStringList styles = QStyleFactory::keys(); diff --git a/launcher/ui/widgets/InfoFrame.cpp b/launcher/ui/widgets/InfoFrame.cpp index fdc581b41..9c041bfe9 100644 --- a/launcher/ui/widgets/InfoFrame.cpp +++ b/launcher/ui/widgets/InfoFrame.cpp @@ -47,6 +47,8 @@ InfoFrame::InfoFrame(QWidget *parent) : ui->setupUi(this); ui->descriptionLabel->setHidden(true); ui->nameLabel->setHidden(true); + ui->licenseLabel->setHidden(true); + ui->issueTrackerLabel->setHidden(true); updateHiddenState(); } @@ -88,7 +90,41 @@ void InfoFrame::updateWithMod(Mod const& m) setDescription(m.description()); } - setImage(); + setImage(m.icon({64,64})); + + auto licenses = m.licenses(); + QString licenseText = ""; + if (!licenses.empty()) { + for (auto l : licenses) { + if (!licenseText.isEmpty()) { + licenseText += "\n"; // add newline between licenses + } + if (!l.name.isEmpty()) { + if (l.url.isEmpty()) { + licenseText += l.name; + } else { + licenseText += "" + l.name + ""; + } + } else if (!l.url.isEmpty()) { + licenseText += "" + l.url + ""; + } + if (!l.description.isEmpty() && l.description != l.name) { + licenseText += " " + l.description; + } + } + } + if (!licenseText.isEmpty()) { + setLicense(tr("License: %1").arg(licenseText)); + } else { + setLicense(); + } + + QString issueTracker = ""; + if (!m.issueTracker().isEmpty()) { + issueTracker += tr("Report issues to: "); + issueTracker += "" + m.issueTracker() + ""; + } + setIssueTracker(issueTracker); } void InfoFrame::updateWithResource(const Resource& resource) @@ -177,16 +213,16 @@ void InfoFrame::clear() setName(); setDescription(); setImage(); + setLicense(); + setIssueTracker(); } void InfoFrame::updateHiddenState() { - if(ui->descriptionLabel->isHidden() && ui->nameLabel->isHidden()) - { + if (ui->descriptionLabel->isHidden() && ui->nameLabel->isHidden() && ui->licenseLabel->isHidden() && + ui->issueTrackerLabel->isHidden()) { setHidden(true); - } - else - { + } else { setHidden(false); } } @@ -251,6 +287,66 @@ void InfoFrame::setDescription(QString text) ui->descriptionLabel->setText(labeltext); } +void InfoFrame::setLicense(QString text) +{ + if(text.isEmpty()) + { + ui->licenseLabel->setHidden(true); + updateHiddenState(); + return; + } + else + { + ui->licenseLabel->setHidden(false); + updateHiddenState(); + } + ui->licenseLabel->setToolTip(""); + QString intermediatetext = text.trimmed(); + bool prev(false); + QChar rem('\n'); + QString finaltext; + finaltext.reserve(intermediatetext.size()); + foreach(const QChar& c, intermediatetext) + { + if(c == rem && prev){ + continue; + } + prev = c == rem; + finaltext += c; + } + QString labeltext; + labeltext.reserve(300); + if(finaltext.length() > 290) + { + ui->licenseLabel->setOpenExternalLinks(false); + ui->licenseLabel->setTextFormat(Qt::TextFormat::RichText); + m_description = text; + // This allows injecting HTML here. + labeltext.append("" + finaltext.left(287) + "..."); + QObject::connect(ui->licenseLabel, &QLabel::linkActivated, this, &InfoFrame::licenseEllipsisHandler); + } + else + { + ui->licenseLabel->setTextFormat(Qt::TextFormat::AutoText); + labeltext.append(finaltext); + } + ui->licenseLabel->setText(labeltext); +} + +void InfoFrame::setIssueTracker(QString text) +{ + if(text.isEmpty()) + { + ui->issueTrackerLabel->setHidden(true); + } + else + { + ui->issueTrackerLabel->setText(text); + ui->issueTrackerLabel->setHidden(false); + } + updateHiddenState(); +} + void InfoFrame::setImage(QPixmap img) { if (img.isNull()) { @@ -275,6 +371,20 @@ void InfoFrame::descriptionEllipsisHandler(QString link) } } +void InfoFrame::licenseEllipsisHandler(QString link) +{ + if(!m_current_box) + { + m_current_box = CustomMessageBox::selectable(this, "", m_license); + connect(m_current_box, &QMessageBox::finished, this, &InfoFrame::boxClosed); + m_current_box->show(); + } + else + { + m_current_box->setText(m_license); + } +} + void InfoFrame::boxClosed(int result) { m_current_box = nullptr; diff --git a/launcher/ui/widgets/InfoFrame.h b/launcher/ui/widgets/InfoFrame.h index 84523e281..7eb679a9d 100644 --- a/launcher/ui/widgets/InfoFrame.h +++ b/launcher/ui/widgets/InfoFrame.h @@ -36,6 +36,8 @@ class InfoFrame : public QFrame { void setName(QString text = {}); void setDescription(QString text = {}); void setImage(QPixmap img = {}); + void setLicense(QString text = {}); + void setIssueTracker(QString text = {}); void clear(); @@ -48,6 +50,7 @@ class InfoFrame : public QFrame { public slots: void descriptionEllipsisHandler(QString link); + void licenseEllipsisHandler(QString link); void boxClosed(int result); private: @@ -56,5 +59,6 @@ class InfoFrame : public QFrame { private: Ui::InfoFrame* ui; QString m_description; + QString m_license; class QMessageBox* m_current_box = nullptr; }; diff --git a/launcher/ui/widgets/InfoFrame.ui b/launcher/ui/widgets/InfoFrame.ui index 9e407ce90..c4d8c83d3 100644 --- a/launcher/ui/widgets/InfoFrame.ui +++ b/launcher/ui/widgets/InfoFrame.ui @@ -35,25 +35,28 @@ 0 - - + + + + + 0 + 0 + + + + + 64 + 64 + + - - Qt::RichText + + false - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - - true - - - true - - - Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + 0 @@ -82,28 +85,69 @@
- - - - - 0 - 0 - - - - - 64 - 64 - - + + - - false + + Qt::RichText - - 0 + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + true + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + + + Qt::RichText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + true + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + + + Qt::RichText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + true + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse diff --git a/launcher/ui/widgets/JavaSettingsWidget.cpp b/launcher/ui/widgets/JavaSettingsWidget.cpp index 159943198..c94fdd8d5 100644 --- a/launcher/ui/widgets/JavaSettingsWidget.cpp +++ b/launcher/ui/widgets/JavaSettingsWidget.cpp @@ -46,7 +46,7 @@ void JavaSettingsWidget::setupUi() m_verticalLayout = new QVBoxLayout(this); m_verticalLayout->setObjectName(QStringLiteral("verticalLayout")); - m_versionWidget = new VersionSelectWidget(this); + m_versionWidget = new VersionSelectWidget(true, this); m_verticalLayout->addWidget(m_versionWidget); m_horizontalLayout = new QHBoxLayout(); diff --git a/launcher/ui/widgets/ModListView.cpp b/launcher/ui/widgets/ModListView.cpp index 09b03a76a..80a918b64 100644 --- a/launcher/ui/widgets/ModListView.cpp +++ b/launcher/ui/widgets/ModListView.cpp @@ -14,9 +14,6 @@ */ #include "ModListView.h" - -#include "minecraft/mod/ModFolderModel.h" - #include #include #include @@ -65,17 +62,13 @@ void ModListView::setModel ( QAbstractItemModel* model ) for(int i = 1; i < head->count(); i++) head->setSectionResizeMode(i, QHeaderView::ResizeToContents); } +} - auto real_model = model; - if (auto proxy_model = dynamic_cast(model); proxy_model) - real_model = proxy_model->sourceModel(); - - if (auto mod_model = dynamic_cast(real_model); mod_model) { - connect(mod_model, &ModFolderModel::updateFinished, this, [this, mod_model]{ - auto mods = mod_model->allMods(); - // Hide the 'Provider' column if no mod has a defined provider! - setColumnHidden(ModFolderModel::Columns::ProviderColumn, - std::none_of(mods.constBegin(), mods.constEnd(), [](auto const mod){ return mod->provider().has_value(); })); - }); +void ModListView::setResizeModes(const QList &modes) +{ + auto head = header(); + for(int i = 0; i < modes.count(); i++) { + head->setSectionResizeMode(i, modes[i]); } } + diff --git a/launcher/ui/widgets/ModListView.h b/launcher/ui/widgets/ModListView.h index 881e092f7..3f0b3b0e1 100644 --- a/launcher/ui/widgets/ModListView.h +++ b/launcher/ui/widgets/ModListView.h @@ -14,6 +14,7 @@ */ #pragma once +#include #include class ModListView: public QTreeView @@ -22,4 +23,5 @@ class ModListView: public QTreeView public: explicit ModListView ( QWidget* parent = 0 ); virtual void setModel ( QAbstractItemModel* model ); + virtual void setResizeModes (const QList& modes); }; diff --git a/launcher/ui/widgets/PageContainer.cpp b/launcher/ui/widgets/PageContainer.cpp index b9b17b423..b98c9796f 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 id, QString info) { + if (m_currentPage && id == m_currentPage->id()) + 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 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 97e294dcf..ad74d43a2 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 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 d1ff9dbc0..0085d6b2b 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 f668edf66..196055ea4 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 0e126c65b..06e58d221 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 + * Copyright (C) 2023 TheKodeToad * * 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 404860d90..a956ddb3c 100644 --- a/launcher/ui/widgets/VersionSelectWidget.cpp +++ b/launcher/ui/widgets/VersionSelectWidget.cpp @@ -1,15 +1,20 @@ #include "VersionSelectWidget.h" +#include +#include +#include +#include #include #include -#include #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 e75efc6f6..be4ba768d 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 * - * 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 . + * + * 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 #include +#include #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 e0616b6ea..7bad1440c 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; - - cmakeFlags = - lib.optionals (msaClientID != "") ["-DLauncher_MSA_CLIENT_ID=${msaClientID}"] - ++ lib.optionals (lib.versionOlder qtbase.version "6") ["-DLauncher_QT_VERSION_MAJOR=5"]; - - 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 - ''; - - 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]}" + ... +}: { + imports = [ + ./dev.nix + ./distribution.nix ]; - 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]; + _module.args = { + # User-friendly version number. + version = builtins.substring 0 8 self.lastModifiedDate; }; + + perSystem = {system, ...}: { + # Nixpkgs instantiated for supported systems with our overlay. + _module.args.pkgs = import inputs.nixpkgs { + inherit system; + overlays = [self.overlays.default]; + }; + }; + + # Supported systems. + systems = [ + "x86_64-linux" + "x86_64-darwin" + "aarch64-linux" + # Disabled due to qtbase being currently broken for "aarch64-darwin." + # "aarch64-darwin" + ]; } diff --git a/nix/dev.nix b/nix/dev.nix new file mode 100644 index 000000000..635c6bb41 --- /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 000000000..0f2e26f3e --- /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 7162a6cf1..000000000 --- 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 000000000..edc266dc4 --- /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/genicons.sh b/program_info/genicons.sh index 42592c4ee..fe8d2e35e 100755 --- a/program_info/genicons.sh +++ b/program_info/genicons.sh @@ -67,7 +67,4 @@ else fi # replace icon in themes -for dir in ../launcher/resources/*/scalable -do - cp -v org.prismlauncher.PrismLauncher.svg "$dir/launcher.svg" -done +cp -v org.prismlauncher.PrismLauncher.svg "../launcher/resources/multimc/scalable/launcher.svg" diff --git a/program_info/win_install.nsi.in b/program_info/win_install.nsi.in index 1d902d5d9..d3b5c256f 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 39a2b6e9a..d97a8dc6c 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/CMakeLists.txt b/tests/CMakeLists.txt index 36a3b0f8d..a26a49fec 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -9,9 +9,6 @@ ecm_add_test(GZip_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}:: ecm_add_test(GradleSpecifier_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME GradleSpecifier) -ecm_add_test(PackageManifest_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test - TEST_NAME PackageManifest) - ecm_add_test(MojangVersionFormat_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME MojangVersionFormat) diff --git a/tests/INIFile_test.cpp b/tests/INIFile_test.cpp index 4be8133c0..95730e244 100644 --- a/tests/INIFile_test.cpp +++ b/tests/INIFile_test.cpp @@ -1,24 +1,19 @@ #include -#include -#include #include +#include +#include +#include +#include +#include "FileSystem.h" #include -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 list_numbers = {1, 2, 3, 10}; + QList 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 out_list_numbers = QVariantUtils::toList(f2.get("list_numbers", QVariantUtils::fromList(QList()))); qDebug() << "OutNumbersList" << out_list_numbers; QCOMPARE(out_list_strings, list_strings); QCOMPARE(out_list_numbers, list_numbers); } + + void test_SaveAlreadyExistingFile() + { + QString fileContent = R"(InstanceType=OneSix +iconKey=vanillia_icon +name=Minecraft Vanillia +OverrideCommands=true +PreLaunchCommand="$INST_JAVA" -jar packwiz-installer-bootstrap.jar link +Wrapperommand=)"; + fileContent += "\""; + fileContent += +R"(\"$INST_JAVA\" -jar packwiz-installer-bootstrap.jar link =)"; + fileContent += "\"\n"; +#if defined(Q_OS_WIN) + QString fileName = "test_SaveAlreadyExistingFile.ini"; + QFile file(fileName); + QCOMPARE(file.open(QFile::WriteOnly | QFile::Text), true); +#else + QTemporaryFile file; + QCOMPARE(file.open(), true); + QCOMPARE(file.fileName().isEmpty(), false); + QString fileName = file.fileName(); +#endif + QTextStream stream(&file); + stream << fileContent; + file.close(); + + // load + INIFile f1; + f1.loadFile(fileName); + QCOMPARE(f1.get("PreLaunchCommand", "NOT SET").toString(), "\"$INST_JAVA\" -jar packwiz-installer-bootstrap.jar link"); + QCOMPARE(f1.get("Wrapperommand", "NOT SET").toString(), "\"$INST_JAVA\" -jar packwiz-installer-bootstrap.jar link ="); + f1.saveFile(fileName); + INIFile f2; + f2.loadFile(fileName); + QCOMPARE(f2.get("PreLaunchCommand", "NOT SET").toString(), "\"$INST_JAVA\" -jar packwiz-installer-bootstrap.jar link"); + QCOMPARE(f2.get("Wrapperommand", "NOT SET").toString(), "\"$INST_JAVA\" -jar packwiz-installer-bootstrap.jar link ="); + QCOMPARE(f2.get("ConfigVersion", "NOT SET").toString(), "1.2"); +#if defined(Q_OS_WIN) + FS::deletePath(fileName); +#endif + } + + void test_SaveAlreadyExistingFileWithSpecialChars() + { +#if defined(Q_OS_WIN) + QString fileName = "test_SaveAlreadyExistingFileWithSpecialChars.ini"; +#else + QTemporaryFile file; + QCOMPARE(file.open(), true); + QCOMPARE(file.fileName().isEmpty(), false); + QString fileName = file.fileName(); + file.close(); +#endif + QSettings settings{ fileName, QSettings::Format::IniFormat }; + settings.setFallbacksEnabled(false); + + settings.setValue("simple", "value1"); + settings.setValue("withQuotes", R"("value2" with quotes)"); + settings.setValue("withSpecialCharacters", "env mesa=true"); + settings.setValue("withSpecialCharacters2", "1,2,3,4"); + settings.setValue("withSpecialCharacters2", "1;2;3;4"); + settings.setValue("withAll", "val=\"$INST_JAVA\" -jar; ls "); + + settings.sync(); + + QCOMPARE(settings.status(), QSettings::Status::NoError); + + // load + INIFile f1; + f1.loadFile(fileName); + for (auto key : settings.allKeys()) + QCOMPARE(f1.get(key, "NOT SET").toString(), settings.value(key).toString()); + f1.saveFile(fileName); + INIFile f2; + f2.loadFile(fileName); + for (auto key : settings.allKeys()) + QCOMPARE(f2.get(key, "NOT SET").toString(), settings.value(key).toString()); + QCOMPARE(f2.get("ConfigVersion", "NOT SET").toString(), "1.2"); +#if defined(Q_OS_WIN) + FS::deletePath(fileName); +#endif + } + + void test_SaveAlreadyExistingFileWithSpecialCharsV1() + { + QString fileContent = R"(InstanceType=OneSix +ConfigVersion=1.1 +iconKey=vanillia_icon +name=Minecraft Vanillia +OverrideCommands=true +PreLaunchCommand=)"; + fileContent += "\"\\\"env mesa=true\\\"\"\n"; + +#if defined(Q_OS_WIN) + QString fileName = "test_SaveAlreadyExistingFileWithSpecialCharsV1.ini"; + QFile file(fileName); + QCOMPARE(file.open(QFile::WriteOnly | QFile::Text), true); +#else + QTemporaryFile file; + QCOMPARE(file.open(), true); + QCOMPARE(file.fileName().isEmpty(), false); + QString fileName = file.fileName(); +#endif + QTextStream stream(&file); + stream << fileContent; + file.close(); + + // load + INIFile f1; + f1.loadFile(fileName); + QCOMPARE(f1.get("PreLaunchCommand", "NOT SET").toString(), "env mesa=true"); + QCOMPARE(f1.get("ConfigVersion", "NOT SET").toString(), "1.2"); +#if defined(Q_OS_WIN) + FS::deletePath(fileName); +#endif + } }; QTEST_GUILESS_MAIN(IniFileTest) diff --git a/tests/PackageManifest_test.cpp b/tests/PackageManifest_test.cpp deleted file mode 100644 index e38abf808..000000000 --- a/tests/PackageManifest_test.cpp +++ /dev/null @@ -1,343 +0,0 @@ -#include -#include - -#include - -using namespace mojang_files; - -QDebug operator<<(QDebug debug, const Path &path) -{ - debug << path.toString(); - return debug; -} - -class PackageManifestTest : public QObject -{ - Q_OBJECT - -private slots: - void test_parse(); - void test_parse_file(); - void test_inspect(); -#ifndef Q_OS_WIN32 - void test_inspect_symlinks(); -#endif - void mkdir_deep(); - void rmdir_deep(); - - void identical_file(); - void changed_file(); - void added_file(); - void removed_file(); -}; - -namespace { -QByteArray basic_manifest = R"END( -{ - "files": { - "a/b.txt": { - "type": "file", - "downloads": { - "raw": { - "url": "http://dethware.org/b.txt", - "sha1": "da39a3ee5e6b4b0d3255bfef95601890afd80709", - "size": 0 - } - }, - "executable": true - }, - "a/b/c": { - "type": "directory" - }, - "a/b/c.txt": { - "type": "link", - "target": "../b.txt" - } - } -} -)END"; -} - -void PackageManifestTest::test_parse() -{ - auto manifest = Package::fromManifestContents(basic_manifest); - QVERIFY(manifest.valid == true); - QVERIFY(manifest.files.size() == 1); - QVERIFY(manifest.files.count(Path("a/b.txt"))); - auto &file = manifest.files[Path("a/b.txt")]; - QVERIFY(file.executable == true); - QVERIFY(file.hash == "da39a3ee5e6b4b0d3255bfef95601890afd80709"); - QVERIFY(file.size == 0); - QVERIFY(manifest.folders.size() == 4); - QVERIFY(manifest.folders.count(Path("."))); - QVERIFY(manifest.folders.count(Path("a"))); - QVERIFY(manifest.folders.count(Path("a/b"))); - QVERIFY(manifest.folders.count(Path("a/b/c"))); - QVERIFY(manifest.symlinks.size() == 1); - auto symlinkPath = Path("a/b/c.txt"); - QVERIFY(manifest.symlinks.count(symlinkPath)); - auto &symlink = manifest.symlinks[symlinkPath]; - QVERIFY(symlink == Path("../b.txt")); - QVERIFY(manifest.sources.size() == 1); -} - -void PackageManifestTest::test_parse_file() { - auto path = QFINDTESTDATA("testdata/PackageManifest/1.8.0_202-x64.json"); - auto manifest = Package::fromManifestFile(path); - QVERIFY(manifest.valid == true); -} - - -void PackageManifestTest::test_inspect() { - auto path = QFINDTESTDATA("testdata/PackageManifest/inspect_win/"); - auto manifest = Package::fromInspectedFolder(path); - QVERIFY(manifest.valid == true); - QVERIFY(manifest.files.size() == 2); - QVERIFY(manifest.files.count(Path("a/b.txt"))); - auto &file1 = manifest.files[Path("a/b.txt")]; - QVERIFY(file1.executable == false); - QVERIFY(file1.hash == "da39a3ee5e6b4b0d3255bfef95601890afd80709"); - QVERIFY(file1.size == 0); - QVERIFY(manifest.files.count(Path("a/b/b.txt"))); - auto &file2 = manifest.files[Path("a/b/b.txt")]; - QVERIFY(file2.executable == false); - QVERIFY(file2.hash == "da39a3ee5e6b4b0d3255bfef95601890afd80709"); - QVERIFY(file2.size == 0); - QVERIFY(manifest.folders.size() == 3); - QVERIFY(manifest.folders.count(Path("."))); - QVERIFY(manifest.folders.count(Path("a"))); - QVERIFY(manifest.folders.count(Path("a/b"))); - QVERIFY(manifest.symlinks.size() == 0); -} - -#ifndef Q_OS_WIN32 -void PackageManifestTest::test_inspect_symlinks() { - auto path = QFINDTESTDATA("testdata/PackageManifest/inspect/"); - auto manifest = Package::fromInspectedFolder(path); - QVERIFY(manifest.valid == true); - QVERIFY(manifest.files.size() == 1); - QVERIFY(manifest.files.count(Path("a/b.txt"))); - auto &file = manifest.files[Path("a/b.txt")]; - QVERIFY(file.executable == true); - QVERIFY(file.hash == "da39a3ee5e6b4b0d3255bfef95601890afd80709"); - QVERIFY(file.size == 0); - QVERIFY(manifest.folders.size() == 3); - QVERIFY(manifest.folders.count(Path("."))); - QVERIFY(manifest.folders.count(Path("a"))); - QVERIFY(manifest.folders.count(Path("a/b"))); - QVERIFY(manifest.symlinks.size() == 1); - QVERIFY(manifest.symlinks.count(Path("a/b/b.txt"))); - qDebug() << manifest.symlinks[Path("a/b/b.txt")]; - QVERIFY(manifest.symlinks[Path("a/b/b.txt")] == Path("../b.txt")); -} -#endif - -void PackageManifestTest::mkdir_deep() { - - Package from; - auto to = Package::fromManifestContents(R"END( -{ - "files": { - "a/b/c/d/e": { - "type": "directory" - } - } -} -)END"); - auto operations = UpdateOperations::resolve(from, to); - QVERIFY(operations.deletes.size() == 0); - QVERIFY(operations.rmdirs.size() == 0); - - QVERIFY(operations.mkdirs.size() == 6); - QVERIFY(operations.mkdirs[0] == Path(".")); - QVERIFY(operations.mkdirs[1] == Path("a")); - QVERIFY(operations.mkdirs[2] == Path("a/b")); - QVERIFY(operations.mkdirs[3] == Path("a/b/c")); - QVERIFY(operations.mkdirs[4] == Path("a/b/c/d")); - QVERIFY(operations.mkdirs[5] == Path("a/b/c/d/e")); - - QVERIFY(operations.downloads.size() == 0); - QVERIFY(operations.mklinks.size() == 0); - QVERIFY(operations.executable_fixes.size() == 0); -} - -void PackageManifestTest::rmdir_deep() { - - Package to; - auto from = Package::fromManifestContents(R"END( -{ - "files": { - "a/b/c/d/e": { - "type": "directory" - } - } -} -)END"); - auto operations = UpdateOperations::resolve(from, to); - QVERIFY(operations.deletes.size() == 0); - - QVERIFY(operations.rmdirs.size() == 6); - QVERIFY(operations.rmdirs[0] == Path("a/b/c/d/e")); - QVERIFY(operations.rmdirs[1] == Path("a/b/c/d")); - QVERIFY(operations.rmdirs[2] == Path("a/b/c")); - QVERIFY(operations.rmdirs[3] == Path("a/b")); - QVERIFY(operations.rmdirs[4] == Path("a")); - QVERIFY(operations.rmdirs[5] == Path(".")); - - QVERIFY(operations.mkdirs.size() == 0); - QVERIFY(operations.downloads.size() == 0); - QVERIFY(operations.mklinks.size() == 0); - QVERIFY(operations.executable_fixes.size() == 0); -} - -void PackageManifestTest::identical_file() { - QByteArray manifest = R"END( -{ - "files": { - "a/b/c/d/empty.txt": { - "type": "file", - "downloads": { - "raw": { - "url": "http://dethware.org/empty.txt", - "sha1": "da39a3ee5e6b4b0d3255bfef95601890afd80709", - "size": 0 - } - }, - "executable": false - } - } -} -)END"; - auto from = Package::fromManifestContents(manifest); - auto to = Package::fromManifestContents(manifest); - auto operations = UpdateOperations::resolve(from, to); - QVERIFY(operations.deletes.size() == 0); - QVERIFY(operations.rmdirs.size() == 0); - QVERIFY(operations.mkdirs.size() == 0); - QVERIFY(operations.downloads.size() == 0); - QVERIFY(operations.mklinks.size() == 0); - QVERIFY(operations.executable_fixes.size() == 0); -} - -void PackageManifestTest::changed_file() { - auto from = Package::fromManifestContents(R"END( -{ - "files": { - "a/b/c/d/file": { - "type": "file", - "downloads": { - "raw": { - "url": "http://dethware.org/empty.txt", - "sha1": "da39a3ee5e6b4b0d3255bfef95601890afd80709", - "size": 0 - } - }, - "executable": false - } - } -} -)END"); - auto to = Package::fromManifestContents(R"END( -{ - "files": { - "a/b/c/d/file": { - "type": "file", - "downloads": { - "raw": { - "url": "http://dethware.org/space.txt", - "sha1": "dd122581c8cd44d0227f9c305581ffcb4b6f1b46", - "size": 1 - } - }, - "executable": false - } - } -} -)END"); - auto operations = UpdateOperations::resolve(from, to); - QVERIFY(operations.deletes.size() == 1); - QCOMPARE(operations.deletes[0], Path("a/b/c/d/file")); - QVERIFY(operations.rmdirs.size() == 0); - QVERIFY(operations.mkdirs.size() == 0); - QVERIFY(operations.downloads.size() == 1); - QVERIFY(operations.mklinks.size() == 0); - QVERIFY(operations.executable_fixes.size() == 0); -} - -void PackageManifestTest::added_file() { - auto from = Package::fromManifestContents(R"END( -{ - "files": { - "a/b/c/d": { - "type": "directory" - } - } -} -)END"); - auto to = Package::fromManifestContents(R"END( -{ - "files": { - "a/b/c/d/file": { - "type": "file", - "downloads": { - "raw": { - "url": "http://dethware.org/space.txt", - "sha1": "dd122581c8cd44d0227f9c305581ffcb4b6f1b46", - "size": 1 - } - }, - "executable": false - } - } -} -)END"); - auto operations = UpdateOperations::resolve(from, to); - QVERIFY(operations.deletes.size() == 0); - QVERIFY(operations.rmdirs.size() == 0); - QVERIFY(operations.mkdirs.size() == 0); - QVERIFY(operations.downloads.size() == 1); - QVERIFY(operations.mklinks.size() == 0); - QVERIFY(operations.executable_fixes.size() == 0); -} - -void PackageManifestTest::removed_file() { - auto from = Package::fromManifestContents(R"END( -{ - "files": { - "a/b/c/d/file": { - "type": "file", - "downloads": { - "raw": { - "url": "http://dethware.org/space.txt", - "sha1": "dd122581c8cd44d0227f9c305581ffcb4b6f1b46", - "size": 1 - } - }, - "executable": false - } - } -} -)END"); - auto to = Package::fromManifestContents(R"END( -{ - "files": { - "a/b/c/d": { - "type": "directory" - } - } -} -)END"); - auto operations = UpdateOperations::resolve(from, to); - QVERIFY(operations.deletes.size() == 1); - QCOMPARE(operations.deletes[0], Path("a/b/c/d/file")); - QVERIFY(operations.rmdirs.size() == 0); - QVERIFY(operations.mkdirs.size() == 0); - QVERIFY(operations.downloads.size() == 0); - QVERIFY(operations.mklinks.size() == 0); - QVERIFY(operations.executable_fixes.size() == 0); -} - -QTEST_GUILESS_MAIN(PackageManifestTest) - -#include "PackageManifest_test.moc" -