Merge branch 'release-7.x' of https://github.com/PrismLauncher/PrismLauncher into develop
This commit is contained in:
commit
cc85b44ff2
16
.github/workflows/build.yml
vendored
16
.github/workflows/build.yml
vendored
@ -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: ''
|
||||
|
||||
@ -374,6 +374,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 == ''
|
||||
@ -386,6 +388,10 @@ jobs:
|
||||
Copy-Item D:/a/PollyMC/Qt/Tools/OpenSSL/Win_x86/bin/libcrypto-1_1.dll -Destination libcrypto-1_1.dll
|
||||
Copy-Item D:/a/PollyMC/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'
|
||||
@ -410,6 +416,7 @@ 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 == ''
|
||||
@ -417,6 +424,8 @@ jobs:
|
||||
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'
|
||||
run: |
|
||||
@ -436,6 +445,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 ../PollyMC.tar.gz *
|
||||
@ -445,6 +455,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 ../PollyMC-portable.tar.gz *
|
||||
|
@ -1,9 +1,10 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
/*
|
||||
* PolyMC - Minecraft Launcher
|
||||
* Prism Launcher - Minecraft Launcher
|
||||
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
|
||||
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
|
||||
* Copyright (C) 2022 Lenny McLennington <lenny@sneed.church>
|
||||
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@ -37,6 +38,7 @@
|
||||
|
||||
#pragma once
|
||||
#include <QString>
|
||||
#include <QList>
|
||||
|
||||
/**
|
||||
* \brief The Config class holds all the build-time information passed from the build system.
|
||||
@ -166,6 +168,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";
|
||||
|
||||
|
15
default.nix
15
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
|
||||
|
62
flake.lock
generated
62
flake.lock
generated
@ -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"
|
||||
|
78
flake.nix
78
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];};
|
||||
}
|
||||
|
@ -535,6 +535,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(MODPACKSCH_SOURCES
|
||||
@ -738,6 +740,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
|
||||
@ -927,6 +933,8 @@ SET(LAUNCHER_SOURCES
|
||||
ui/dialogs/ElybyLoginDialog.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
|
||||
@ -1074,6 +1082,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
|
||||
|
47
launcher/FastFileIconProvider.cpp
Normal file
47
launcher/FastFileIconProvider.cpp
Normal file
@ -0,0 +1,47 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
/*
|
||||
* Prism Launcher - Minecraft Launcher
|
||||
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "FastFileIconProvider.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QStyle>
|
||||
|
||||
QIcon FastFileIconProvider::icon(const QFileInfo& info) const
|
||||
{
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 4, 0)
|
||||
bool link = info.isSymbolicLink() || info.isAlias() || info.isShortcut();
|
||||
#else
|
||||
// in versions prior to 6.4 we don't have access to isAlias
|
||||
bool link = info.isSymLink();
|
||||
#endif
|
||||
QStyle::StandardPixmap icon;
|
||||
|
||||
if (info.isDir()) {
|
||||
if (link)
|
||||
icon = QStyle::SP_DirLinkIcon;
|
||||
else
|
||||
icon = QStyle::SP_DirIcon;
|
||||
} else {
|
||||
if (link)
|
||||
icon = QStyle::SP_FileLinkIcon;
|
||||
else
|
||||
icon = QStyle::SP_FileIcon;
|
||||
}
|
||||
|
||||
return QApplication::style()->standardIcon(icon);
|
||||
}
|
26
launcher/FastFileIconProvider.h
Normal file
26
launcher/FastFileIconProvider.h
Normal file
@ -0,0 +1,26 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
/*
|
||||
* Prism Launcher - Minecraft Launcher
|
||||
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QFileIconProvider>
|
||||
|
||||
class FastFileIconProvider : public QFileIconProvider {
|
||||
public:
|
||||
QIcon icon(const QFileInfo& info) const override;
|
||||
};
|
256
launcher/FileIgnoreProxy.cpp
Normal file
256
launcher/FileIgnoreProxy.cpp
Normal file
@ -0,0 +1,256 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
/*
|
||||
* Prism Launcher - Minecraft Launcher
|
||||
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
|
||||
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* This file incorporates work covered by the following copyright and
|
||||
* permission notice:
|
||||
*
|
||||
* Copyright 2013-2021 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "FileIgnoreProxy.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QFileSystemModel>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QStack>
|
||||
#include "FileSystem.h"
|
||||
#include "SeparatorPrefixTree.h"
|
||||
#include "StringUtils.h"
|
||||
|
||||
FileIgnoreProxy::FileIgnoreProxy(QString root, QObject* parent) : QSortFilterProxyModel(parent), root(root) {}
|
||||
// NOTE: Sadly, we have to do sorting ourselves.
|
||||
bool FileIgnoreProxy::lessThan(const QModelIndex& left, const QModelIndex& right) const
|
||||
{
|
||||
QFileSystemModel* fsm = qobject_cast<QFileSystemModel*>(sourceModel());
|
||||
if (!fsm) {
|
||||
return QSortFilterProxyModel::lessThan(left, right);
|
||||
}
|
||||
bool asc = sortOrder() == Qt::AscendingOrder ? true : false;
|
||||
|
||||
QFileInfo leftFileInfo = fsm->fileInfo(left);
|
||||
QFileInfo rightFileInfo = fsm->fileInfo(right);
|
||||
|
||||
if (!leftFileInfo.isDir() && rightFileInfo.isDir()) {
|
||||
return !asc;
|
||||
}
|
||||
if (leftFileInfo.isDir() && !rightFileInfo.isDir()) {
|
||||
return asc;
|
||||
}
|
||||
|
||||
// sort and proxy model breaks the original model...
|
||||
if (sortColumn() == 0) {
|
||||
return StringUtils::naturalCompare(leftFileInfo.fileName(), rightFileInfo.fileName(), Qt::CaseInsensitive) < 0;
|
||||
}
|
||||
if (sortColumn() == 1) {
|
||||
auto leftSize = leftFileInfo.size();
|
||||
auto rightSize = rightFileInfo.size();
|
||||
if ((leftSize == rightSize) || (leftFileInfo.isDir() && rightFileInfo.isDir())) {
|
||||
return StringUtils::naturalCompare(leftFileInfo.fileName(), rightFileInfo.fileName(), Qt::CaseInsensitive) < 0 ? asc : !asc;
|
||||
}
|
||||
return leftSize < rightSize;
|
||||
}
|
||||
return QSortFilterProxyModel::lessThan(left, right);
|
||||
}
|
||||
|
||||
Qt::ItemFlags FileIgnoreProxy::flags(const QModelIndex& index) const
|
||||
{
|
||||
if (!index.isValid())
|
||||
return Qt::NoItemFlags;
|
||||
|
||||
auto sourceIndex = mapToSource(index);
|
||||
Qt::ItemFlags flags = sourceIndex.flags();
|
||||
if (index.column() == 0) {
|
||||
flags |= Qt::ItemIsUserCheckable;
|
||||
if (sourceIndex.model()->hasChildren(sourceIndex)) {
|
||||
flags |= Qt::ItemIsAutoTristate;
|
||||
}
|
||||
}
|
||||
|
||||
return flags;
|
||||
}
|
||||
|
||||
QVariant FileIgnoreProxy::data(const QModelIndex& index, int role) const
|
||||
{
|
||||
QModelIndex sourceIndex = mapToSource(index);
|
||||
|
||||
if (index.column() == 0 && role == Qt::CheckStateRole) {
|
||||
QFileSystemModel* fsm = qobject_cast<QFileSystemModel*>(sourceModel());
|
||||
auto blockedPath = relPath(fsm->filePath(sourceIndex));
|
||||
auto cover = blocked.cover(blockedPath);
|
||||
if (!cover.isNull()) {
|
||||
return QVariant(Qt::Unchecked);
|
||||
} else if (blocked.exists(blockedPath)) {
|
||||
return QVariant(Qt::PartiallyChecked);
|
||||
} else {
|
||||
return QVariant(Qt::Checked);
|
||||
}
|
||||
}
|
||||
|
||||
return sourceIndex.data(role);
|
||||
}
|
||||
|
||||
bool FileIgnoreProxy::setData(const QModelIndex& index, const QVariant& value, int role)
|
||||
{
|
||||
if (index.column() == 0 && role == Qt::CheckStateRole) {
|
||||
Qt::CheckState state = static_cast<Qt::CheckState>(value.toInt());
|
||||
return setFilterState(index, state);
|
||||
}
|
||||
|
||||
QModelIndex sourceIndex = mapToSource(index);
|
||||
return QSortFilterProxyModel::sourceModel()->setData(sourceIndex, value, role);
|
||||
}
|
||||
|
||||
QString FileIgnoreProxy::relPath(const QString& path) const
|
||||
{
|
||||
return QDir(root).relativeFilePath(path);
|
||||
}
|
||||
|
||||
bool FileIgnoreProxy::setFilterState(QModelIndex index, Qt::CheckState state)
|
||||
{
|
||||
QFileSystemModel* fsm = qobject_cast<QFileSystemModel*>(sourceModel());
|
||||
|
||||
if (!fsm) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QModelIndex sourceIndex = mapToSource(index);
|
||||
auto blockedPath = relPath(fsm->filePath(sourceIndex));
|
||||
bool changed = false;
|
||||
if (state == Qt::Unchecked) {
|
||||
// blocking a path
|
||||
auto& node = blocked.insert(blockedPath);
|
||||
// get rid of all blocked nodes below
|
||||
node.clear();
|
||||
changed = true;
|
||||
} else if (state == Qt::Checked || state == Qt::PartiallyChecked) {
|
||||
if (!blocked.remove(blockedPath)) {
|
||||
auto cover = blocked.cover(blockedPath);
|
||||
qDebug() << "Blocked by cover" << cover;
|
||||
// uncover
|
||||
blocked.remove(cover);
|
||||
// block all contents, except for any cover
|
||||
QModelIndex rootIndex = fsm->index(FS::PathCombine(root, cover));
|
||||
QModelIndex doing = rootIndex;
|
||||
int row = 0;
|
||||
QStack<QModelIndex> todo;
|
||||
while (1) {
|
||||
auto node = fsm->index(row, 0, doing);
|
||||
if (!node.isValid()) {
|
||||
if (!todo.size()) {
|
||||
break;
|
||||
} else {
|
||||
doing = todo.pop();
|
||||
row = 0;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
auto relpath = relPath(fsm->filePath(node));
|
||||
if (blockedPath.startsWith(relpath)) // cover found?
|
||||
{
|
||||
// continue processing cover later
|
||||
todo.push(node);
|
||||
} else {
|
||||
// or just block this one.
|
||||
blocked.insert(relpath);
|
||||
}
|
||||
row++;
|
||||
}
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
if (changed) {
|
||||
// update the thing
|
||||
emit dataChanged(index, index, { Qt::CheckStateRole });
|
||||
// update everything above index
|
||||
QModelIndex up = index.parent();
|
||||
while (1) {
|
||||
if (!up.isValid())
|
||||
break;
|
||||
emit dataChanged(up, up, { Qt::CheckStateRole });
|
||||
up = up.parent();
|
||||
}
|
||||
// and everything below the index
|
||||
QModelIndex doing = index;
|
||||
int row = 0;
|
||||
QStack<QModelIndex> todo;
|
||||
while (1) {
|
||||
auto node = this->index(row, 0, doing);
|
||||
if (!node.isValid()) {
|
||||
if (!todo.size()) {
|
||||
break;
|
||||
} else {
|
||||
doing = todo.pop();
|
||||
row = 0;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
emit dataChanged(node, node, { Qt::CheckStateRole });
|
||||
todo.push(node);
|
||||
row++;
|
||||
}
|
||||
// siblings and unrelated nodes are ignored
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FileIgnoreProxy::shouldExpand(QModelIndex index)
|
||||
{
|
||||
QModelIndex sourceIndex = mapToSource(index);
|
||||
QFileSystemModel* fsm = qobject_cast<QFileSystemModel*>(sourceModel());
|
||||
if (!fsm) {
|
||||
return false;
|
||||
}
|
||||
auto blockedPath = relPath(fsm->filePath(sourceIndex));
|
||||
auto found = blocked.find(blockedPath);
|
||||
if (found) {
|
||||
return !found->leaf();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void FileIgnoreProxy::setBlockedPaths(QStringList paths)
|
||||
{
|
||||
beginResetModel();
|
||||
blocked.clear();
|
||||
blocked.insert(paths);
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
bool FileIgnoreProxy::filterAcceptsColumn(int source_column, const QModelIndex& source_parent) const
|
||||
{
|
||||
Q_UNUSED(source_parent)
|
||||
|
||||
// adjust the columns you want to filter out here
|
||||
// return false for those that will be hidden
|
||||
if (source_column == 2 || source_column == 3)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
72
launcher/FileIgnoreProxy.h
Normal file
72
launcher/FileIgnoreProxy.h
Normal file
@ -0,0 +1,72 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
/*
|
||||
* Prism Launcher - Minecraft Launcher
|
||||
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
|
||||
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* This file incorporates work covered by the following copyright and
|
||||
* permission notice:
|
||||
*
|
||||
* Copyright 2013-2021 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QSortFilterProxyModel>
|
||||
#include "SeparatorPrefixTree.h"
|
||||
|
||||
class FileIgnoreProxy : public QSortFilterProxyModel {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
FileIgnoreProxy(QString root, QObject* parent);
|
||||
// NOTE: Sadly, we have to do sorting ourselves.
|
||||
bool lessThan(const QModelIndex& left, const QModelIndex& right) const;
|
||||
|
||||
virtual Qt::ItemFlags flags(const QModelIndex& index) const;
|
||||
|
||||
virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const;
|
||||
virtual bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole);
|
||||
|
||||
QString relPath(const QString& path) const;
|
||||
|
||||
bool setFilterState(QModelIndex index, Qt::CheckState state);
|
||||
|
||||
bool shouldExpand(QModelIndex index);
|
||||
|
||||
void setBlockedPaths(QStringList paths);
|
||||
|
||||
inline const SeparatorPrefixTree<'/'>& blockedPaths() const { return blocked; }
|
||||
inline SeparatorPrefixTree<'/'>& blockedPaths() { return blocked; }
|
||||
|
||||
protected:
|
||||
bool filterAcceptsColumn(int source_column, const QModelIndex& source_parent) const;
|
||||
|
||||
private:
|
||||
const QString root;
|
||||
SeparatorPrefixTree<'/'> blocked;
|
||||
};
|
@ -39,7 +39,16 @@ void InstanceCopyTask::executeTask()
|
||||
setStatus(tr("Copying instance %1").arg(m_origInstance->name()));
|
||||
|
||||
auto copySaves = [&]() {
|
||||
FS::copy savesCopy(FS::PathCombine(m_origInstance->instanceRoot(), "saves"), FS::PathCombine(m_stagingPath, "saves"));
|
||||
QFileInfo mcDir(FS::PathCombine(m_stagingPath, "minecraft"));
|
||||
QFileInfo dotMCDir(FS::PathCombine(m_stagingPath, ".minecraft"));
|
||||
|
||||
QString staging_mc_dir;
|
||||
if (mcDir.exists() && !dotMCDir.exists())
|
||||
staging_mc_dir = mcDir.filePath();
|
||||
else
|
||||
staging_mc_dir = dotMCDir.filePath();
|
||||
|
||||
FS::copy savesCopy(FS::PathCombine(m_origInstance->gameRoot(), "saves"), FS::PathCombine(staging_mc_dir, "saves"));
|
||||
savesCopy.followSymlinks(true);
|
||||
|
||||
return savesCopy();
|
||||
@ -123,6 +132,7 @@ void InstanceCopyTask::copyFinished()
|
||||
emitFailed(tr("Instance folder copy failed."));
|
||||
return;
|
||||
}
|
||||
|
||||
// FIXME: shouldn't this be able to report errors?
|
||||
auto instanceSettings = std::make_shared<INISettingsObject>(FS::PathCombine(m_stagingPath, "instance.cfg"));
|
||||
|
||||
@ -134,6 +144,24 @@ void InstanceCopyTask::copyFinished()
|
||||
}
|
||||
if (m_useLinks)
|
||||
inst->addLinkedInstanceId(m_origInstance->id());
|
||||
if (m_useLinks) {
|
||||
auto allowed_symlinks_file = QFileInfo(FS::PathCombine(inst->gameRoot(), "allowed_symlinks.txt"));
|
||||
|
||||
QByteArray allowed_symlinks;
|
||||
if (allowed_symlinks_file.exists()) {
|
||||
allowed_symlinks.append(FS::read(allowed_symlinks_file.filePath()));
|
||||
if (allowed_symlinks.right(1) != "\n")
|
||||
allowed_symlinks.append("\n"); // we want to be on a new line
|
||||
}
|
||||
allowed_symlinks.append(m_origInstance->gameRoot().toUtf8());
|
||||
allowed_symlinks.append("\n");
|
||||
if (allowed_symlinks_file.isSymLink())
|
||||
FS::deletePath(allowed_symlinks_file
|
||||
.filePath()); // we dont want to modify the original. also make sure the resulting file is not itself a link.
|
||||
|
||||
FS::write(allowed_symlinks_file.filePath(), allowed_symlinks);
|
||||
}
|
||||
|
||||
emitSucceeded();
|
||||
}
|
||||
|
||||
|
@ -1,21 +1,21 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
/*
|
||||
* Prism Launcher - Minecraft Launcher
|
||||
* Copyright (c) 2022-2023 flowln <flowlnlnln@gmail.com>
|
||||
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
* Prism Launcher - Minecraft Launcher
|
||||
* Copyright (c) 2022-2023 flowln <flowlnlnln@gmail.com>
|
||||
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "ResourceDownloadTask.h"
|
||||
|
||||
@ -24,14 +24,15 @@
|
||||
#include "minecraft/mod/ModFolderModel.h"
|
||||
#include "minecraft/mod/ResourceFolderModel.h"
|
||||
|
||||
ResourceDownloadTask::ResourceDownloadTask(ModPlatform::IndexedPack pack,
|
||||
ResourceDownloadTask::ResourceDownloadTask(ModPlatform::IndexedPack::Ptr pack,
|
||||
ModPlatform::IndexedVersion version,
|
||||
const std::shared_ptr<ResourceFolderModel> packs,
|
||||
bool is_indexed)
|
||||
: m_pack(std::move(pack)), m_pack_version(std::move(version)), m_pack_model(packs)
|
||||
bool is_indexed,
|
||||
QString custom_target_folder)
|
||||
: m_pack(std::move(pack)), m_pack_version(std::move(version)), m_pack_model(packs), m_custom_target_folder(custom_target_folder)
|
||||
{
|
||||
if (auto model = dynamic_cast<ModFolderModel*>(m_pack_model.get()); model && is_indexed) {
|
||||
m_update_task.reset(new LocalModUpdateTask(model->indexDir(), m_pack, m_pack_version));
|
||||
m_update_task.reset(new LocalModUpdateTask(model->indexDir(), *m_pack, m_pack_version));
|
||||
connect(m_update_task.get(), &LocalModUpdateTask::hasOldMod, this, &ResourceDownloadTask::hasOldResource);
|
||||
|
||||
addTask(m_update_task);
|
||||
@ -40,13 +41,13 @@ ResourceDownloadTask::ResourceDownloadTask(ModPlatform::IndexedPack pack,
|
||||
m_filesNetJob.reset(new NetJob(tr("Resource download"), APPLICATION->network()));
|
||||
m_filesNetJob->setStatus(tr("Downloading resource:\n%1").arg(m_pack_version.downloadUrl));
|
||||
|
||||
QDir dir { m_pack_model->dir() };
|
||||
QDir dir{ m_pack_model->dir() };
|
||||
{
|
||||
// FIXME: Make this more generic. May require adding additional info to IndexedVersion,
|
||||
// or adquiring a reference to the base instance.
|
||||
if (!m_pack_version.custom_target_folder.isEmpty()) {
|
||||
if (!m_custom_target_folder.isEmpty()) {
|
||||
dir.cdUp();
|
||||
dir.cd(m_pack_version.custom_target_folder);
|
||||
dir.cd(m_custom_target_folder);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,44 +1,51 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
/*
|
||||
* Prism Launcher - Minecraft Launcher
|
||||
* Copyright (c) 2022-2023 flowln <flowlnlnln@gmail.com>
|
||||
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
* Prism Launcher - Minecraft Launcher
|
||||
* Copyright (c) 2022-2023 flowln <flowlnlnln@gmail.com>
|
||||
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "net/NetJob.h"
|
||||
#include "tasks/SequentialTask.h"
|
||||
|
||||
#include "modplatform/ModIndex.h"
|
||||
#include "minecraft/mod/tasks/LocalModUpdateTask.h"
|
||||
#include "modplatform/ModIndex.h"
|
||||
|
||||
class ResourceFolderModel;
|
||||
|
||||
class ResourceDownloadTask : public SequentialTask {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ResourceDownloadTask(ModPlatform::IndexedPack pack, ModPlatform::IndexedVersion version, const std::shared_ptr<ResourceFolderModel> packs, bool is_indexed = true);
|
||||
public:
|
||||
explicit ResourceDownloadTask(ModPlatform::IndexedPack::Ptr pack,
|
||||
ModPlatform::IndexedVersion version,
|
||||
const std::shared_ptr<ResourceFolderModel> packs,
|
||||
bool is_indexed = true,
|
||||
QString custom_target_folder = {});
|
||||
const QString& getFilename() const { return m_pack_version.fileName; }
|
||||
const QString& getCustomPath() const { return m_pack_version.custom_target_folder; }
|
||||
const QString& getCustomPath() const { return m_custom_target_folder; }
|
||||
const QVariant& getVersionID() const { return m_pack_version.fileId; }
|
||||
const QString& getName() const { return m_pack->name; }
|
||||
ModPlatform::IndexedPack::Ptr getPack() { return m_pack; }
|
||||
|
||||
private:
|
||||
ModPlatform::IndexedPack m_pack;
|
||||
private:
|
||||
ModPlatform::IndexedPack::Ptr m_pack;
|
||||
ModPlatform::IndexedVersion m_pack_version;
|
||||
const std::shared_ptr<ResourceFolderModel> m_pack_model;
|
||||
QString m_custom_target_folder;
|
||||
|
||||
NetJob::Ptr m_filesNetJob;
|
||||
LocalModUpdateTask::Ptr m_update_task;
|
||||
@ -47,11 +54,8 @@ private:
|
||||
void downloadFailed(QString reason);
|
||||
void downloadSucceeded();
|
||||
|
||||
std::tuple<QString, QString> to_delete {"", ""};
|
||||
std::tuple<QString, QString> to_delete{ "", "" };
|
||||
|
||||
private slots:
|
||||
private slots:
|
||||
void hasOldResource(QString name, QString filename);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
@ -56,10 +56,10 @@ static Version::Ptr parseCommonVersion(const QString &uid, const QJsonObject &ob
|
||||
version->setType(ensureString(obj, "type", QString()));
|
||||
version->setRecommended(ensureBoolean(obj, QString("recommended"), false));
|
||||
version->setVolatile(ensureBoolean(obj, QString("volatile"), false));
|
||||
RequireSet requires, conflicts;
|
||||
parseRequires(obj, &requires, "requires");
|
||||
RequireSet reqs, conflicts;
|
||||
parseRequires(obj, &reqs, "requires");
|
||||
parseRequires(obj, &conflicts, "conflicts");
|
||||
version->setRequires(requires, conflicts);
|
||||
version->setRequires(reqs, conflicts);
|
||||
return version;
|
||||
}
|
||||
|
||||
@ -176,7 +176,6 @@ void parseRequires(const QJsonObject& obj, RequireSet* ptr, const char * keyName
|
||||
{
|
||||
if(obj.contains(keyName))
|
||||
{
|
||||
QSet<QString> requires;
|
||||
auto reqArray = requireArray(obj, keyName);
|
||||
auto iter = reqArray.begin();
|
||||
while(iter != reqArray.end())
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -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))
|
||||
|
@ -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())
|
||||
{
|
||||
|
@ -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
|
||||
|
@ -33,7 +33,9 @@ static const QMap<int, std::pair<Version, Version>> s_pack_format_versions = {
|
||||
{ 4, { Version("1.13"), Version("1.14.4") } }, { 5, { Version("1.15"), Version("1.16.1") } },
|
||||
{ 6, { Version("1.16.2"), Version("1.16.5") } }, { 7, { Version("1.17"), Version("1.17.1") } },
|
||||
{ 8, { Version("1.18"), Version("1.18.1") } }, { 9, { Version("1.18.2"), Version("1.18.2") } },
|
||||
{ 10, { Version("1.19"), Version("1.19.3") } },
|
||||
{ 10, { Version("1.19"), Version("1.19.3") } }, { 11, { Version("23w03a"), Version("23w05a") } },
|
||||
{ 12, { Version("1.19.4"), Version("1.19.4") } }, { 13, { Version("23w12a"), Version("23w14a") } },
|
||||
{ 14, { Version("23w16a"), Version("23w17a") } }, { 15, { Version("1.20"), Version("1.20") } },
|
||||
};
|
||||
|
||||
void DataPack::setPackFormat(int new_format_id)
|
||||
|
@ -18,7 +18,8 @@ static const QMap<int, std::pair<Version, Version>> s_pack_format_versions = {
|
||||
{ 5, { Version("1.15"), Version("1.16.1") } }, { 6, { Version("1.16.2"), Version("1.16.5") } },
|
||||
{ 7, { Version("1.17"), Version("1.17.1") } }, { 8, { Version("1.18"), Version("1.18.2") } },
|
||||
{ 9, { Version("1.19"), Version("1.19.2") } }, { 11, { Version("22w42a"), Version("22w44a") } },
|
||||
{ 12, { Version("1.19.3"), Version("1.19.3") } },
|
||||
{ 12, { Version("1.19.3"), Version("1.19.3") } }, { 13, { Version("1.19.4"), Version("1.19.4") } },
|
||||
{ 14, { Version("1.20"), Version("1.20") } }
|
||||
};
|
||||
|
||||
void ResourcePack::setPackFormat(int new_format_id)
|
||||
|
@ -1,20 +1,20 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
/*
|
||||
* PolyMC - Minecraft Launcher
|
||||
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
* PolyMC - Minecraft Launcher
|
||||
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
@ -69,7 +69,6 @@ struct IndexedVersion {
|
||||
|
||||
// For internal use, not provided by APIs
|
||||
bool is_currently_selected = false;
|
||||
QString custom_target_folder;
|
||||
};
|
||||
|
||||
struct ExtraPackData {
|
||||
@ -116,12 +115,12 @@ struct IndexedPack {
|
||||
if (!versionsLoaded)
|
||||
return false;
|
||||
|
||||
return std::any_of(versions.constBegin(), versions.constEnd(),
|
||||
[](auto const& v) { return v.is_currently_selected; });
|
||||
return std::any_of(versions.constBegin(), versions.constEnd(), [](auto const& v) { return v.is_currently_selected; });
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace ModPlatform
|
||||
|
||||
Q_DECLARE_METATYPE(ModPlatform::IndexedPack)
|
||||
Q_DECLARE_METATYPE(ModPlatform::IndexedPack::Ptr)
|
||||
Q_DECLARE_METATYPE(ModPlatform::ResourceProvider)
|
||||
|
@ -352,7 +352,7 @@ QString PackInstallTask::getVersionForLoader(QString uid)
|
||||
if(m_version.loader.recommended || m_version.loader.latest) {
|
||||
for (int i = 0; i < vlist->versions().size(); i++) {
|
||||
auto version = vlist->versions().at(i);
|
||||
auto reqs = version->requires();
|
||||
auto reqs = version->requiredSet();
|
||||
|
||||
// filter by minecraft version, if the loader depends on a certain version.
|
||||
// not all mod loaders depend on a given Minecraft version, so we won't do this
|
||||
|
@ -3,6 +3,7 @@
|
||||
#include "FlameModIndex.h"
|
||||
|
||||
#include <MurmurHash2.h>
|
||||
#include <memory>
|
||||
|
||||
#include "FileSystem.h"
|
||||
#include "Json.h"
|
||||
@ -129,8 +130,7 @@ void FlameCheckUpdate::executeTask()
|
||||
setStatus(tr("Getting API response from CurseForge for '%1'...").arg(mod->name()));
|
||||
setProgress(i++, m_mods.size());
|
||||
|
||||
ModPlatform::IndexedPack pack{ mod->metadata()->project_id.toString() };
|
||||
auto latest_ver = api.getLatestVersion({ pack, m_game_versions, m_loaders });
|
||||
auto latest_ver = api.getLatestVersion({ { mod->metadata()->project_id.toString() }, m_game_versions, m_loaders });
|
||||
|
||||
// Check if we were aborted while getting the latest version
|
||||
if (m_was_aborted) {
|
||||
@ -156,15 +156,15 @@ void FlameCheckUpdate::executeTask()
|
||||
|
||||
if (!latest_ver.hash.isEmpty() && (mod->metadata()->hash != latest_ver.hash || mod->status() == ModStatus::NotInstalled)) {
|
||||
// Fake pack with the necessary info to pass to the download task :)
|
||||
ModPlatform::IndexedPack pack;
|
||||
pack.name = mod->name();
|
||||
pack.slug = mod->metadata()->slug;
|
||||
pack.addonId = mod->metadata()->project_id;
|
||||
pack.websiteUrl = mod->homeurl();
|
||||
auto pack = std::make_shared<ModPlatform::IndexedPack>();
|
||||
pack->name = mod->name();
|
||||
pack->slug = mod->metadata()->slug;
|
||||
pack->addonId = mod->metadata()->project_id;
|
||||
pack->websiteUrl = mod->homeurl();
|
||||
for (auto& author : mod->authors())
|
||||
pack.authors.append({ author });
|
||||
pack.description = mod->description();
|
||||
pack.provider = ModPlatform::ResourceProvider::FLAME;
|
||||
pack->authors.append({ author });
|
||||
pack->description = mod->description();
|
||||
pack->provider = ModPlatform::ResourceProvider::FLAME;
|
||||
|
||||
auto old_version = mod->version();
|
||||
if (old_version.isEmpty() && mod->status() != ModStatus::NotInstalled) {
|
||||
@ -173,7 +173,7 @@ void FlameCheckUpdate::executeTask()
|
||||
}
|
||||
|
||||
auto download_task = makeShared<ResourceDownloadTask>(pack, latest_ver, m_mods_folder);
|
||||
m_updatable.emplace_back(pack.name, mod->metadata()->hash, old_version, latest_ver.version,
|
||||
m_updatable.emplace_back(pack->name, mod->metadata()->hash, old_version, latest_ver.version,
|
||||
api.getModFileChangelog(latest_ver.addonId.toInt(), latest_ver.fileId.toInt()),
|
||||
ModPlatform::ResourceProvider::FLAME, download_task);
|
||||
}
|
||||
|
@ -54,7 +54,7 @@ void ModrinthCheckUpdate::executeTask()
|
||||
if (mod->metadata()->hash_format != best_hash_type) {
|
||||
auto hash_task = Hashing::createModrinthHasher(mod->fileinfo().absoluteFilePath());
|
||||
connect(hash_task.get(), &Task::succeeded, [&] {
|
||||
QString hash (hash_task->getResult());
|
||||
QString hash(hash_task->getResult());
|
||||
hashes.append(hash);
|
||||
mappings.insert(hash, mod);
|
||||
});
|
||||
@ -67,7 +67,7 @@ void ModrinthCheckUpdate::executeTask()
|
||||
}
|
||||
|
||||
QEventLoop loop;
|
||||
connect(&hashing_task, &Task::finished, [&loop]{ loop.quit(); });
|
||||
connect(&hashing_task, &Task::finished, [&loop] { loop.quit(); });
|
||||
hashing_task.start();
|
||||
loop.exec();
|
||||
|
||||
@ -112,7 +112,8 @@ void ModrinthCheckUpdate::executeTask()
|
||||
// so we may want to filter it
|
||||
QString loader_filter;
|
||||
if (m_loaders.has_value()) {
|
||||
static auto flags = { ResourceAPI::ModLoaderType::Forge, ResourceAPI::ModLoaderType::Fabric, ResourceAPI::ModLoaderType::Quilt };
|
||||
static auto flags = { ResourceAPI::ModLoaderType::Forge, ResourceAPI::ModLoaderType::Fabric,
|
||||
ResourceAPI::ModLoaderType::Quilt };
|
||||
for (auto flag : flags) {
|
||||
if (m_loaders.value().testFlag(flag)) {
|
||||
loader_filter = api.getModLoaderString(flag);
|
||||
@ -122,7 +123,8 @@ void ModrinthCheckUpdate::executeTask()
|
||||
}
|
||||
|
||||
// Currently, we rely on a couple heuristics to determine whether an update is actually available or not:
|
||||
// - The file needs to be preferred: It is either the primary file, or the one found via (explicit) usage of the loader_filter
|
||||
// - The file needs to be preferred: It is either the primary file, or the one found via (explicit) usage of the
|
||||
// loader_filter
|
||||
// - The version reported by the JAR is different from the version reported by the indexed version (it's usually the case)
|
||||
// Such is the pain of having arbitrary files for a given version .-.
|
||||
|
||||
@ -149,19 +151,19 @@ void ModrinthCheckUpdate::executeTask()
|
||||
continue;
|
||||
|
||||
// Fake pack with the necessary info to pass to the download task :)
|
||||
ModPlatform::IndexedPack pack;
|
||||
pack.name = mod->name();
|
||||
pack.slug = mod->metadata()->slug;
|
||||
pack.addonId = mod->metadata()->project_id;
|
||||
pack.websiteUrl = mod->homeurl();
|
||||
auto pack = std::make_shared<ModPlatform::IndexedPack>();
|
||||
pack->name = mod->name();
|
||||
pack->slug = mod->metadata()->slug;
|
||||
pack->addonId = mod->metadata()->project_id;
|
||||
pack->websiteUrl = mod->homeurl();
|
||||
for (auto& author : mod->authors())
|
||||
pack.authors.append({ author });
|
||||
pack.description = mod->description();
|
||||
pack.provider = ModPlatform::ResourceProvider::MODRINTH;
|
||||
pack->authors.append({ author });
|
||||
pack->description = mod->description();
|
||||
pack->provider = ModPlatform::ResourceProvider::MODRINTH;
|
||||
|
||||
auto download_task = makeShared<ResourceDownloadTask>(pack, project_ver, m_mods_folder);
|
||||
|
||||
m_updatable.emplace_back(pack.name, hash, mod->version(), project_ver.version_number, project_ver.changelog,
|
||||
m_updatable.emplace_back(pack->name, hash, mod->version(), project_ver.version_number, project_ver.changelog,
|
||||
ModPlatform::ResourceProvider::MODRINTH, download_task);
|
||||
}
|
||||
}
|
||||
|
323
launcher/modplatform/modrinth/ModrinthPackExportTask.cpp
Normal file
323
launcher/modplatform/modrinth/ModrinthPackExportTask.cpp
Normal file
@ -0,0 +1,323 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
/*
|
||||
* Prism Launcher - Minecraft Launcher
|
||||
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "ModrinthPackExportTask.h"
|
||||
|
||||
#include <QCryptographicHash>
|
||||
#include <QFileInfo>
|
||||
#include <QMessageBox>
|
||||
#include <QtConcurrentRun>
|
||||
#include "Json.h"
|
||||
#include "MMCZip.h"
|
||||
#include "minecraft/PackProfile.h"
|
||||
#include "minecraft/mod/ModFolderModel.h"
|
||||
|
||||
const QStringList ModrinthPackExportTask::PREFIXES({ "mods", "coremods", "resourcepacks", "texturepacks", "shaderpacks" });
|
||||
const QStringList ModrinthPackExportTask::FILE_EXTENSIONS({ "jar", "litemod", "zip" });
|
||||
|
||||
ModrinthPackExportTask::ModrinthPackExportTask(const QString& name,
|
||||
const QString& version,
|
||||
const QString& summary,
|
||||
InstancePtr instance,
|
||||
const QString& output,
|
||||
MMCZip::FilterFunction filter)
|
||||
: name(name)
|
||||
, version(version)
|
||||
, summary(summary)
|
||||
, instance(instance)
|
||||
, mcInstance(dynamic_cast<MinecraftInstance*>(instance.get()))
|
||||
, gameRoot(instance->gameRoot())
|
||||
, output(output)
|
||||
, filter(filter)
|
||||
{}
|
||||
|
||||
void ModrinthPackExportTask::executeTask()
|
||||
{
|
||||
setStatus(tr("Searching for files..."));
|
||||
setProgress(0, 0);
|
||||
collectFiles();
|
||||
}
|
||||
|
||||
bool ModrinthPackExportTask::abort()
|
||||
{
|
||||
if (task != nullptr) {
|
||||
task->abort();
|
||||
task = nullptr;
|
||||
emitAborted();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (buildZipFuture.isRunning()) {
|
||||
buildZipFuture.cancel();
|
||||
// NOTE: Here we don't do `emitAborted()` because it will be done when `buildZipFuture` actually cancels, which may not occur immediately.
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void ModrinthPackExportTask::collectFiles()
|
||||
{
|
||||
setAbortable(false);
|
||||
QCoreApplication::processEvents();
|
||||
|
||||
files.clear();
|
||||
if (!MMCZip::collectFileListRecursively(instance->gameRoot(), nullptr, &files, filter)) {
|
||||
emitFailed(tr("Could not search for files"));
|
||||
return;
|
||||
}
|
||||
|
||||
pendingHashes.clear();
|
||||
resolvedFiles.clear();
|
||||
|
||||
if (mcInstance) {
|
||||
mcInstance->loaderModList()->update();
|
||||
connect(mcInstance->loaderModList().get(), &ModFolderModel::updateFinished, this, &ModrinthPackExportTask::collectHashes);
|
||||
} else
|
||||
collectHashes();
|
||||
}
|
||||
|
||||
void ModrinthPackExportTask::collectHashes()
|
||||
{
|
||||
for (const QFileInfo& file : files) {
|
||||
QCoreApplication::processEvents();
|
||||
|
||||
const QString relative = gameRoot.relativeFilePath(file.absoluteFilePath());
|
||||
// require sensible file types
|
||||
if (!std::any_of(PREFIXES.begin(), PREFIXES.end(),
|
||||
[&relative](const QString& prefix) { return relative.startsWith(prefix + QDir::separator()); }))
|
||||
continue;
|
||||
if (!std::any_of(FILE_EXTENSIONS.begin(), FILE_EXTENSIONS.end(), [&relative](const QString& extension) {
|
||||
return relative.endsWith('.' + extension) || relative.endsWith('.' + extension + ".disabled");
|
||||
})) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QCryptographicHash sha512(QCryptographicHash::Algorithm::Sha512);
|
||||
|
||||
QFile openFile(file.absoluteFilePath());
|
||||
if (!openFile.open(QFile::ReadOnly)) {
|
||||
qWarning() << "Could not open" << file << "for hashing";
|
||||
continue;
|
||||
}
|
||||
|
||||
const QByteArray data = openFile.readAll();
|
||||
if (openFile.error() != QFileDevice::NoError) {
|
||||
qWarning() << "Could not read" << file;
|
||||
continue;
|
||||
}
|
||||
sha512.addData(data);
|
||||
|
||||
auto allMods = mcInstance->loaderModList()->allMods();
|
||||
if (auto modIter = std::find_if(allMods.begin(), allMods.end(), [&file](Mod* mod) { return mod->fileinfo() == file; });
|
||||
modIter != allMods.end()) {
|
||||
const Mod* mod = *modIter;
|
||||
if (mod->metadata() != nullptr) {
|
||||
QUrl& url = mod->metadata()->url;
|
||||
// ensure the url is permitted on modrinth.com
|
||||
if (!url.isEmpty() && BuildConfig.MODRINTH_MRPACK_HOSTS.contains(url.host())) {
|
||||
qDebug() << "Resolving" << relative << "from index";
|
||||
|
||||
QCryptographicHash sha1(QCryptographicHash::Algorithm::Sha1);
|
||||
sha1.addData(data);
|
||||
|
||||
ResolvedFile file{ sha1.result().toHex(), sha512.result().toHex(), url.toString(), openFile.size() };
|
||||
resolvedFiles[relative] = file;
|
||||
|
||||
// nice! we've managed to resolve based on local metadata!
|
||||
// no need to enqueue it
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
qDebug() << "Enqueueing" << relative << "for Modrinth query";
|
||||
pendingHashes[relative] = sha512.result().toHex();
|
||||
}
|
||||
|
||||
setAbortable(true);
|
||||
makeApiRequest();
|
||||
}
|
||||
|
||||
void ModrinthPackExportTask::makeApiRequest()
|
||||
{
|
||||
if (pendingHashes.isEmpty())
|
||||
buildZip();
|
||||
else {
|
||||
QByteArray* response = new QByteArray;
|
||||
task = api.currentVersions(pendingHashes.values(), "sha512", response);
|
||||
connect(task.get(), &NetJob::succeeded, [this, response]() { parseApiResponse(response); });
|
||||
connect(task.get(), &NetJob::failed, this, &ModrinthPackExportTask::emitFailed);
|
||||
task->start();
|
||||
}
|
||||
}
|
||||
|
||||
void ModrinthPackExportTask::parseApiResponse(const QByteArray* response)
|
||||
{
|
||||
task = nullptr;
|
||||
|
||||
try {
|
||||
const QJsonDocument doc = Json::requireDocument(*response);
|
||||
|
||||
QMapIterator<QString, QString> iterator(pendingHashes);
|
||||
while (iterator.hasNext()) {
|
||||
iterator.next();
|
||||
|
||||
const QJsonObject obj = doc[iterator.value()].toObject();
|
||||
if (obj.isEmpty())
|
||||
continue;
|
||||
|
||||
const QJsonArray files = obj["files"].toArray();
|
||||
if (auto fileIter = std::find_if(files.begin(), files.end(),
|
||||
[&iterator](const QJsonValue& file) { return file["hashes"]["sha512"] == iterator.value(); });
|
||||
fileIter != files.end()) {
|
||||
// map the file to the url
|
||||
resolvedFiles[iterator.key()] =
|
||||
ResolvedFile{ fileIter->toObject()["hashes"].toObject()["sha1"].toString(), iterator.value(),
|
||||
fileIter->toObject()["url"].toString(), fileIter->toObject()["size"].toInt() };
|
||||
}
|
||||
}
|
||||
} catch (const Json::JsonException& e) {
|
||||
emitFailed(tr("Failed to parse versions response: %1").arg(e.what()));
|
||||
return;
|
||||
}
|
||||
pendingHashes.clear();
|
||||
buildZip();
|
||||
}
|
||||
|
||||
void ModrinthPackExportTask::buildZip()
|
||||
{
|
||||
setStatus(tr("Adding files..."));
|
||||
|
||||
buildZipFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this]() {
|
||||
QuaZip zip(output);
|
||||
if (!zip.open(QuaZip::mdCreate)) {
|
||||
QFile::remove(output);
|
||||
return BuildZipResult(tr("Could not create file"));
|
||||
}
|
||||
|
||||
if (buildZipFuture.isCanceled())
|
||||
return BuildZipResult();
|
||||
|
||||
QuaZipFile indexFile(&zip);
|
||||
if (!indexFile.open(QIODevice::WriteOnly, QuaZipNewInfo("modrinth.index.json"))) {
|
||||
QFile::remove(output);
|
||||
return BuildZipResult(tr("Could not create index"));
|
||||
}
|
||||
indexFile.write(generateIndex());
|
||||
|
||||
size_t progress = 0;
|
||||
for (const QFileInfo& file : files) {
|
||||
if (buildZipFuture.isCanceled()) {
|
||||
QFile::remove(output);
|
||||
return BuildZipResult();
|
||||
}
|
||||
|
||||
setProgress(progress, files.length());
|
||||
const QString relative = gameRoot.relativeFilePath(file.absoluteFilePath());
|
||||
if (!resolvedFiles.contains(relative) && !JlCompress::compressFile(&zip, file.absoluteFilePath(), "overrides/" + relative)) {
|
||||
QFile::remove(output);
|
||||
return BuildZipResult(tr("Could not read and compress %1").arg(relative));
|
||||
}
|
||||
progress++;
|
||||
}
|
||||
|
||||
zip.close();
|
||||
|
||||
if (zip.getZipError() != 0) {
|
||||
QFile::remove(output);
|
||||
return BuildZipResult(tr("A zip error occurred"));
|
||||
}
|
||||
|
||||
return BuildZipResult();
|
||||
});
|
||||
connect(&buildZipWatcher, &QFutureWatcher<BuildZipResult>::finished, this, &ModrinthPackExportTask::finish);
|
||||
buildZipWatcher.setFuture(buildZipFuture);
|
||||
}
|
||||
|
||||
void ModrinthPackExportTask::finish()
|
||||
{
|
||||
if (buildZipFuture.isCanceled())
|
||||
emitAborted();
|
||||
else {
|
||||
const BuildZipResult result = buildZipFuture.result();
|
||||
if (result.has_value())
|
||||
emitFailed(result.value());
|
||||
else
|
||||
emitSucceeded();
|
||||
}
|
||||
}
|
||||
|
||||
QByteArray ModrinthPackExportTask::generateIndex()
|
||||
{
|
||||
QJsonObject obj;
|
||||
obj["formatVersion"] = 1;
|
||||
obj["game"] = "minecraft";
|
||||
obj["name"] = name;
|
||||
obj["versionId"] = version;
|
||||
if (!summary.isEmpty())
|
||||
obj["summary"] = summary;
|
||||
|
||||
if (mcInstance) {
|
||||
auto profile = mcInstance->getPackProfile();
|
||||
// collect all supported components
|
||||
const ComponentPtr minecraft = profile->getComponent("net.minecraft");
|
||||
const ComponentPtr quilt = profile->getComponent("org.quiltmc.quilt-loader");
|
||||
const ComponentPtr fabric = profile->getComponent("net.fabricmc.fabric-loader");
|
||||
const ComponentPtr forge = profile->getComponent("net.minecraftforge");
|
||||
|
||||
// convert all available components to mrpack dependencies
|
||||
QJsonObject dependencies;
|
||||
if (minecraft != nullptr)
|
||||
dependencies["minecraft"] = minecraft->m_version;
|
||||
if (quilt != nullptr)
|
||||
dependencies["quilt-loader"] = quilt->m_version;
|
||||
if (fabric != nullptr)
|
||||
dependencies["fabric-loader"] = fabric->m_version;
|
||||
if (forge != nullptr)
|
||||
dependencies["forge"] = forge->m_version;
|
||||
|
||||
obj["dependencies"] = dependencies;
|
||||
}
|
||||
|
||||
QJsonArray files;
|
||||
QMapIterator<QString, ResolvedFile> iterator(resolvedFiles);
|
||||
while (iterator.hasNext()) {
|
||||
iterator.next();
|
||||
|
||||
const ResolvedFile& value = iterator.value();
|
||||
|
||||
QJsonObject file;
|
||||
QString path = iterator.key();
|
||||
path.replace(QDir::separator(), "/");
|
||||
file["path"] = path;
|
||||
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);
|
||||
}
|
77
launcher/modplatform/modrinth/ModrinthPackExportTask.h
Normal file
77
launcher/modplatform/modrinth/ModrinthPackExportTask.h
Normal file
@ -0,0 +1,77 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
/*
|
||||
* Prism Launcher - Minecraft Launcher
|
||||
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QFuture>
|
||||
#include <QFutureWatcher>
|
||||
#include "BaseInstance.h"
|
||||
#include "MMCZip.h"
|
||||
#include "minecraft/MinecraftInstance.h"
|
||||
#include "modplatform/modrinth/ModrinthAPI.h"
|
||||
#include "tasks/Task.h"
|
||||
|
||||
class ModrinthPackExportTask : public Task {
|
||||
public:
|
||||
ModrinthPackExportTask(const QString& name,
|
||||
const QString& version,
|
||||
const QString& summary,
|
||||
InstancePtr instance,
|
||||
const QString& output,
|
||||
MMCZip::FilterFunction filter);
|
||||
|
||||
protected:
|
||||
void executeTask() override;
|
||||
bool abort() override;
|
||||
|
||||
private:
|
||||
struct ResolvedFile {
|
||||
QString sha1, sha512, url;
|
||||
qint64 size;
|
||||
};
|
||||
|
||||
static const QStringList PREFIXES;
|
||||
static const QStringList FILE_EXTENSIONS;
|
||||
|
||||
// inputs
|
||||
const QString name, version, summary;
|
||||
const InstancePtr instance;
|
||||
MinecraftInstance* mcInstance;
|
||||
const QDir gameRoot;
|
||||
const QString output;
|
||||
const MMCZip::FilterFunction filter;
|
||||
|
||||
typedef std::optional<QString> BuildZipResult;
|
||||
|
||||
ModrinthAPI api;
|
||||
QFileInfoList files;
|
||||
QMap<QString, QString> pendingHashes;
|
||||
QMap<QString, ResolvedFile> resolvedFiles;
|
||||
Task::Ptr task;
|
||||
QFuture<BuildZipResult> buildZipFuture;
|
||||
QFutureWatcher<BuildZipResult> buildZipWatcher;
|
||||
|
||||
void collectFiles();
|
||||
void collectHashes();
|
||||
void makeApiRequest();
|
||||
void parseApiResponse(const QByteArray* response);
|
||||
void buildZip();
|
||||
void finish();
|
||||
|
||||
QByteArray generateIndex();
|
||||
};
|
@ -190,7 +190,7 @@ struct TranslationsModel::Private
|
||||
std::unique_ptr<QTranslator> m_qt_translator;
|
||||
std::unique_ptr<QTranslator> m_app_translator;
|
||||
|
||||
Net::Download::Ptr m_index_task;
|
||||
Net::Download* m_index_task;
|
||||
QString m_downloadingTranslation;
|
||||
NetJob::Ptr m_dl_job;
|
||||
NetJob::Ptr m_index_job;
|
||||
@ -673,8 +673,9 @@ void TranslationsModel::downloadIndex()
|
||||
d->m_index_job.reset(new NetJob("Translations Index", APPLICATION->network()));
|
||||
MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("translations", "index_v2.json");
|
||||
entry->setStale(true);
|
||||
d->m_index_task = Net::Download::makeCached(QUrl(BuildConfig.TRANSLATIONS_BASE_URL + "index_v2.json"), entry);
|
||||
d->m_index_job->addNetAction(d->m_index_task);
|
||||
auto task = Net::Download::makeCached(QUrl(BuildConfig.TRANSLATIONS_BASE_URL + "index_v2.json"), entry);
|
||||
d->m_index_task = task.get();
|
||||
d->m_index_job->addNetAction(task);
|
||||
connect(d->m_index_job.get(), &NetJob::failed, this, &TranslationsModel::indexFailed);
|
||||
connect(d->m_index_job.get(), &NetJob::succeeded, this, &TranslationsModel::indexReceived);
|
||||
d->m_index_job->start();
|
||||
|
@ -2,7 +2,7 @@
|
||||
/*
|
||||
* Prism Launcher - Minecraft Launcher
|
||||
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
|
||||
* Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
|
||||
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@ -107,6 +107,7 @@
|
||||
#include "ui/dialogs/CopyInstanceDialog.h"
|
||||
#include "ui/dialogs/EditAccountDialog.h"
|
||||
#include "ui/dialogs/ExportInstanceDialog.h"
|
||||
#include "ui/dialogs/ExportMrPackDialog.h"
|
||||
#include "ui/dialogs/ImportResourceDialog.h"
|
||||
#include "ui/themes/ITheme.h"
|
||||
#include "ui/themes/ThemeManager.h"
|
||||
@ -199,7 +200,6 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi
|
||||
helpMenuButton->setPopupMode(QToolButton::InstantPopup);
|
||||
|
||||
auto accountMenuButton = dynamic_cast<QToolButton*>(ui->mainToolBar->widgetForAction(ui->actionAccountsButton));
|
||||
ui->actionAccountsButton->setMenu(ui->accountsMenu);
|
||||
accountMenuButton->setPopupMode(QToolButton::InstantPopup);
|
||||
}
|
||||
|
||||
@ -397,6 +397,8 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi
|
||||
// removing this looks stupid
|
||||
view->setFocus();
|
||||
|
||||
ui->actionExportInstance->setMenu(ui->exportInstanceMenu);
|
||||
|
||||
retranslateUi();
|
||||
}
|
||||
|
||||
@ -414,15 +416,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 +425,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());
|
||||
|
||||
@ -673,6 +672,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 +695,8 @@ void MainWindow::repopulateAccountsMenu()
|
||||
}
|
||||
}
|
||||
|
||||
QActionGroup* accountsGroup = new QActionGroup(this);
|
||||
|
||||
if (accounts->count() <= 0)
|
||||
{
|
||||
ui->actionNoAccountsAdded->setEnabled(false);
|
||||
@ -702,6 +712,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 +741,7 @@ void MainWindow::repopulateAccountsMenu()
|
||||
|
||||
ui->actionNoDefaultAccount->setData(-1);
|
||||
ui->actionNoDefaultAccount->setChecked(!defaultAccount);
|
||||
ui->actionNoDefaultAccount->setActionGroup(accountsGroup);
|
||||
|
||||
ui->accountsMenu->addAction(ui->actionNoDefaultAccount);
|
||||
|
||||
@ -737,6 +749,8 @@ void MainWindow::repopulateAccountsMenu()
|
||||
|
||||
ui->accountsMenu->addSeparator();
|
||||
ui->accountsMenu->addAction(ui->actionManageAccounts);
|
||||
|
||||
accountsButtonMenu->addActions(ui->accountsMenu->actions());
|
||||
}
|
||||
|
||||
void MainWindow::updatesAllowedChanged(bool allowed)
|
||||
@ -1359,7 +1373,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 +1382,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)
|
||||
|
@ -1,7 +1,8 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
/*
|
||||
* PolyMC - Minecraft Launcher
|
||||
* Prism Launcher - Minecraft Launcher
|
||||
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
|
||||
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@ -151,7 +152,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();
|
||||
|
||||
|
@ -150,6 +150,10 @@
|
||||
<addaction name="actionChangeInstGroup"/>
|
||||
<addaction name="actionViewSelectedInstFolder"/>
|
||||
<addaction name="actionExportInstance"/>
|
||||
<widget class="QMenu" name="exportInstanceMenu">
|
||||
<addaction name="actionExportInstanceZip"/>
|
||||
<addaction name="actionExportInstanceMrPack"/>
|
||||
</widget>
|
||||
<addaction name="actionCopyInstance"/>
|
||||
<addaction name="actionDeleteInstance"/>
|
||||
<addaction name="actionCreateInstanceShortcut"/>
|
||||
@ -459,10 +463,17 @@
|
||||
<string>E&xport...</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Export the selected instance as a zip file.</string>
|
||||
<string>Export the selected instance to supported formats.</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+E</string>
|
||||
</action>
|
||||
<action name="actionExportInstanceZip">
|
||||
<property name="text">
|
||||
<string>Prism Launcher (zip)</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionExportInstanceMrPack">
|
||||
<property name="text">
|
||||
<string>Modrinth (mrpack)</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionCreateInstanceShortcut">
|
||||
|
@ -1,7 +1,8 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
/*
|
||||
* PolyMC - Minecraft Launcher
|
||||
* Prism Launcher - Minecraft Launcher
|
||||
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
|
||||
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@ -46,301 +47,21 @@
|
||||
#include <QSaveFile>
|
||||
#include <QStack>
|
||||
#include <QFileInfo>
|
||||
|
||||
#include "StringUtils.h"
|
||||
#include "SeparatorPrefixTree.h"
|
||||
#include "Application.h"
|
||||
#include <icons/IconList.h>
|
||||
#include <FileSystem.h>
|
||||
|
||||
class PackIgnoreProxy : public QSortFilterProxyModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
PackIgnoreProxy(InstancePtr instance, QObject *parent) : QSortFilterProxyModel(parent)
|
||||
{
|
||||
m_instance = instance;
|
||||
}
|
||||
// NOTE: Sadly, we have to do sorting ourselves.
|
||||
bool lessThan(const QModelIndex &left, const QModelIndex &right) const
|
||||
{
|
||||
QFileSystemModel *fsm = qobject_cast<QFileSystemModel *>(sourceModel());
|
||||
if (!fsm)
|
||||
{
|
||||
return QSortFilterProxyModel::lessThan(left, right);
|
||||
}
|
||||
bool asc = sortOrder() == Qt::AscendingOrder ? true : false;
|
||||
|
||||
QFileInfo leftFileInfo = fsm->fileInfo(left);
|
||||
QFileInfo rightFileInfo = fsm->fileInfo(right);
|
||||
|
||||
if (!leftFileInfo.isDir() && rightFileInfo.isDir())
|
||||
{
|
||||
return !asc;
|
||||
}
|
||||
if (leftFileInfo.isDir() && !rightFileInfo.isDir())
|
||||
{
|
||||
return asc;
|
||||
}
|
||||
|
||||
// sort and proxy model breaks the original model...
|
||||
if (sortColumn() == 0)
|
||||
{
|
||||
return StringUtils::naturalCompare(leftFileInfo.fileName(), rightFileInfo.fileName(),
|
||||
Qt::CaseInsensitive) < 0;
|
||||
}
|
||||
if (sortColumn() == 1)
|
||||
{
|
||||
auto leftSize = leftFileInfo.size();
|
||||
auto rightSize = rightFileInfo.size();
|
||||
if ((leftSize == rightSize) || (leftFileInfo.isDir() && rightFileInfo.isDir()))
|
||||
{
|
||||
return StringUtils::naturalCompare(leftFileInfo.fileName(),
|
||||
rightFileInfo.fileName(),
|
||||
Qt::CaseInsensitive) < 0
|
||||
? asc
|
||||
: !asc;
|
||||
}
|
||||
return leftSize < rightSize;
|
||||
}
|
||||
return QSortFilterProxyModel::lessThan(left, right);
|
||||
}
|
||||
|
||||
virtual Qt::ItemFlags flags(const QModelIndex &index) const
|
||||
{
|
||||
if (!index.isValid())
|
||||
return Qt::NoItemFlags;
|
||||
|
||||
auto sourceIndex = mapToSource(index);
|
||||
Qt::ItemFlags flags = sourceIndex.flags();
|
||||
if (index.column() == 0)
|
||||
{
|
||||
flags |= Qt::ItemIsUserCheckable;
|
||||
if (sourceIndex.model()->hasChildren(sourceIndex))
|
||||
{
|
||||
flags |= Qt::ItemIsAutoTristate;
|
||||
}
|
||||
}
|
||||
|
||||
return flags;
|
||||
}
|
||||
|
||||
virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const
|
||||
{
|
||||
QModelIndex sourceIndex = mapToSource(index);
|
||||
|
||||
if (index.column() == 0 && role == Qt::CheckStateRole)
|
||||
{
|
||||
QFileSystemModel *fsm = qobject_cast<QFileSystemModel *>(sourceModel());
|
||||
auto blockedPath = relPath(fsm->filePath(sourceIndex));
|
||||
auto cover = blocked.cover(blockedPath);
|
||||
if (!cover.isNull())
|
||||
{
|
||||
return QVariant(Qt::Unchecked);
|
||||
}
|
||||
else if (blocked.exists(blockedPath))
|
||||
{
|
||||
return QVariant(Qt::PartiallyChecked);
|
||||
}
|
||||
else
|
||||
{
|
||||
return QVariant(Qt::Checked);
|
||||
}
|
||||
}
|
||||
|
||||
return sourceIndex.data(role);
|
||||
}
|
||||
|
||||
virtual bool setData(const QModelIndex &index, const QVariant &value,
|
||||
int role = Qt::EditRole)
|
||||
{
|
||||
if (index.column() == 0 && role == Qt::CheckStateRole)
|
||||
{
|
||||
Qt::CheckState state = static_cast<Qt::CheckState>(value.toInt());
|
||||
return setFilterState(index, state);
|
||||
}
|
||||
|
||||
QModelIndex sourceIndex = mapToSource(index);
|
||||
return QSortFilterProxyModel::sourceModel()->setData(sourceIndex, value, role);
|
||||
}
|
||||
|
||||
QString relPath(const QString &path) const
|
||||
{
|
||||
QString prefix = QDir().absoluteFilePath(m_instance->instanceRoot());
|
||||
prefix += '/';
|
||||
if (!path.startsWith(prefix))
|
||||
{
|
||||
return QString();
|
||||
}
|
||||
return path.mid(prefix.size());
|
||||
}
|
||||
|
||||
bool setFilterState(QModelIndex index, Qt::CheckState state)
|
||||
{
|
||||
QFileSystemModel *fsm = qobject_cast<QFileSystemModel *>(sourceModel());
|
||||
|
||||
if (!fsm)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
QModelIndex sourceIndex = mapToSource(index);
|
||||
auto blockedPath = relPath(fsm->filePath(sourceIndex));
|
||||
bool changed = false;
|
||||
if (state == Qt::Unchecked)
|
||||
{
|
||||
// blocking a path
|
||||
auto &node = blocked.insert(blockedPath);
|
||||
// get rid of all blocked nodes below
|
||||
node.clear();
|
||||
changed = true;
|
||||
}
|
||||
else if (state == Qt::Checked || state == Qt::PartiallyChecked)
|
||||
{
|
||||
if (!blocked.remove(blockedPath))
|
||||
{
|
||||
auto cover = blocked.cover(blockedPath);
|
||||
qDebug() << "Blocked by cover" << cover;
|
||||
// uncover
|
||||
blocked.remove(cover);
|
||||
// block all contents, except for any cover
|
||||
QModelIndex rootIndex =
|
||||
fsm->index(FS::PathCombine(m_instance->instanceRoot(), cover));
|
||||
QModelIndex doing = rootIndex;
|
||||
int row = 0;
|
||||
QStack<QModelIndex> todo;
|
||||
while (1)
|
||||
{
|
||||
auto node = fsm->index(row, 0, doing);
|
||||
if (!node.isValid())
|
||||
{
|
||||
if (!todo.size())
|
||||
{
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
doing = todo.pop();
|
||||
row = 0;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
auto relpath = relPath(fsm->filePath(node));
|
||||
if (blockedPath.startsWith(relpath)) // cover found?
|
||||
{
|
||||
// continue processing cover later
|
||||
todo.push(node);
|
||||
}
|
||||
else
|
||||
{
|
||||
// or just block this one.
|
||||
blocked.insert(relpath);
|
||||
}
|
||||
row++;
|
||||
}
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
if (changed)
|
||||
{
|
||||
// update the thing
|
||||
emit dataChanged(index, index, {Qt::CheckStateRole});
|
||||
// update everything above index
|
||||
QModelIndex up = index.parent();
|
||||
while (1)
|
||||
{
|
||||
if (!up.isValid())
|
||||
break;
|
||||
emit dataChanged(up, up, {Qt::CheckStateRole});
|
||||
up = up.parent();
|
||||
}
|
||||
// and everything below the index
|
||||
QModelIndex doing = index;
|
||||
int row = 0;
|
||||
QStack<QModelIndex> todo;
|
||||
while (1)
|
||||
{
|
||||
auto node = this->index(row, 0, doing);
|
||||
if (!node.isValid())
|
||||
{
|
||||
if (!todo.size())
|
||||
{
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
doing = todo.pop();
|
||||
row = 0;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
emit dataChanged(node, node, {Qt::CheckStateRole});
|
||||
todo.push(node);
|
||||
row++;
|
||||
}
|
||||
// siblings and unrelated nodes are ignored
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool shouldExpand(QModelIndex index)
|
||||
{
|
||||
QModelIndex sourceIndex = mapToSource(index);
|
||||
QFileSystemModel *fsm = qobject_cast<QFileSystemModel *>(sourceModel());
|
||||
if (!fsm)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
auto blockedPath = relPath(fsm->filePath(sourceIndex));
|
||||
auto found = blocked.find(blockedPath);
|
||||
if(found)
|
||||
{
|
||||
return !found->leaf();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void setBlockedPaths(QStringList paths)
|
||||
{
|
||||
beginResetModel();
|
||||
blocked.clear();
|
||||
blocked.insert(paths);
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
const SeparatorPrefixTree<'/'> & blockedPaths() const
|
||||
{
|
||||
return blocked;
|
||||
}
|
||||
|
||||
protected:
|
||||
bool filterAcceptsColumn(int source_column, const QModelIndex &source_parent) const
|
||||
{
|
||||
Q_UNUSED(source_parent)
|
||||
|
||||
// adjust the columns you want to filter out here
|
||||
// return false for those that will be hidden
|
||||
if (source_column == 2 || source_column == 3)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
InstancePtr m_instance;
|
||||
SeparatorPrefixTree<'/'> blocked;
|
||||
};
|
||||
|
||||
ExportInstanceDialog::ExportInstanceDialog(InstancePtr instance, QWidget *parent)
|
||||
: QDialog(parent), ui(new Ui::ExportInstanceDialog), m_instance(instance)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
auto model = new QFileSystemModel(this);
|
||||
proxyModel = new PackIgnoreProxy(m_instance, this);
|
||||
model->setIconProvider(&icons);
|
||||
auto root = instance->instanceRoot();
|
||||
proxyModel = new FileIgnoreProxy(root, this);
|
||||
loadPackIgnore();
|
||||
proxyModel->setSourceModel(model);
|
||||
auto root = instance->instanceRoot();
|
||||
ui->treeView->setModel(proxyModel);
|
||||
ui->treeView->setRootIndex(proxyModel->mapFromSource(model->index(root)));
|
||||
ui->treeView->sortByColumn(0, Qt::AscendingOrder);
|
||||
@ -404,22 +125,11 @@ bool ExportInstanceDialog::doExport()
|
||||
|
||||
const QString output = QFileDialog::getSaveFileName(
|
||||
this, tr("Export %1").arg(m_instance->name()),
|
||||
FS::PathCombine(QDir::homePath(), name + ".zip"), "Zip (*.zip)", nullptr, QFileDialog::DontConfirmOverwrite);
|
||||
FS::PathCombine(QDir::homePath(), name + ".zip"), "Zip (*.zip)", nullptr);
|
||||
if (output.isEmpty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (QFile::exists(output))
|
||||
{
|
||||
int ret =
|
||||
QMessageBox::question(this, tr("Overwrite?"),
|
||||
tr("This file already exists. Do you want to overwrite it?"),
|
||||
QMessageBox::No, QMessageBox::Yes);
|
||||
if (ret == QMessageBox::No)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
SaveIcon(m_instance);
|
||||
|
||||
@ -511,5 +221,3 @@ void ExportInstanceDialog::savePackIgnore()
|
||||
qWarning() << e.cause();
|
||||
}
|
||||
}
|
||||
|
||||
#include "ExportInstanceDialog.moc"
|
||||
|
@ -1,16 +1,36 @@
|
||||
/* Copyright 2013-2021 MultiMC Contributors
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
/*
|
||||
* Prism Launcher - Minecraft Launcher
|
||||
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3.
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* This file incorporates work covered by the following copyright and
|
||||
* permission notice:
|
||||
*
|
||||
* Copyright 2013-2021 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
@ -18,9 +38,10 @@
|
||||
#include <QDialog>
|
||||
#include <QModelIndex>
|
||||
#include <memory>
|
||||
#include "FileIgnoreProxy.h"
|
||||
#include "FastFileIconProvider.h"
|
||||
|
||||
class BaseInstance;
|
||||
class PackIgnoreProxy;
|
||||
typedef std::shared_ptr<BaseInstance> InstancePtr;
|
||||
|
||||
namespace Ui
|
||||
@ -47,7 +68,8 @@ private:
|
||||
private:
|
||||
Ui::ExportInstanceDialog *ui;
|
||||
InstancePtr m_instance;
|
||||
PackIgnoreProxy * proxyModel;
|
||||
FileIgnoreProxy * proxyModel;
|
||||
FastFileIconProvider icons;
|
||||
|
||||
private slots:
|
||||
void rowsInserted(QModelIndex parent, int top, int bottom);
|
||||
|
123
launcher/ui/dialogs/ExportMrPackDialog.cpp
Normal file
123
launcher/ui/dialogs/ExportMrPackDialog.cpp
Normal file
@ -0,0 +1,123 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
/*
|
||||
* Prism Launcher - Minecraft Launcher
|
||||
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "ExportMrPackDialog.h"
|
||||
#include "minecraft/mod/ModFolderModel.h"
|
||||
#include "ui/dialogs/CustomMessageBox.h"
|
||||
#include "ui/dialogs/ProgressDialog.h"
|
||||
#include "ui_ExportMrPackDialog.h"
|
||||
|
||||
#include <QFileDialog>
|
||||
#include <QFileSystemModel>
|
||||
#include <QJsonDocument>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include "FastFileIconProvider.h"
|
||||
#include "FileSystem.h"
|
||||
#include "MMCZip.h"
|
||||
#include "modplatform/modrinth/ModrinthPackExportTask.h"
|
||||
|
||||
ExportMrPackDialog::ExportMrPackDialog(InstancePtr instance, QWidget* parent)
|
||||
: QDialog(parent), instance(instance), ui(new Ui::ExportMrPackDialog)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
ui->name->setText(instance->name());
|
||||
ui->summary->setText(instance->notes().split(QRegularExpression("\\r?\\n"))[0]);
|
||||
|
||||
// ensure a valid pack is generated
|
||||
// the name and version fields mustn't be empty
|
||||
connect(ui->name, &QLineEdit::textEdited, this, &ExportMrPackDialog::validate);
|
||||
connect(ui->version, &QLineEdit::textEdited, this, &ExportMrPackDialog::validate);
|
||||
// the instance name can technically be empty
|
||||
validate();
|
||||
|
||||
QFileSystemModel* model = new QFileSystemModel(this);
|
||||
model->setIconProvider(&icons);
|
||||
|
||||
// use the game root - everything outside cannot be exported
|
||||
const QDir root(instance->gameRoot());
|
||||
proxy = new FileIgnoreProxy(instance->gameRoot(), this);
|
||||
proxy->setSourceModel(model);
|
||||
|
||||
const QDir::Filters filter(QDir::AllEntries | QDir::NoDotAndDotDot | QDir::AllDirs | QDir::Hidden);
|
||||
|
||||
for (const QString& file : root.entryList(filter)) {
|
||||
if (!(file == "mods" || file == "coremods" || file == "datapacks" || file == "config" || file == "options.txt" ||
|
||||
file == "servers.dat"))
|
||||
proxy->blockedPaths().insert(file);
|
||||
}
|
||||
|
||||
MinecraftInstance* mcInstance = dynamic_cast<MinecraftInstance*>(instance.get());
|
||||
if (mcInstance) {
|
||||
const QDir index = mcInstance->loaderModList()->indexDir();
|
||||
if (index.exists())
|
||||
proxy->blockedPaths().insert(root.relativeFilePath(index.absolutePath()));
|
||||
}
|
||||
|
||||
ui->treeView->setModel(proxy);
|
||||
ui->treeView->setRootIndex(proxy->mapFromSource(model->index(instance->gameRoot())));
|
||||
ui->treeView->sortByColumn(0, Qt::AscendingOrder);
|
||||
|
||||
model->setFilter(filter);
|
||||
model->setRootPath(instance->gameRoot());
|
||||
|
||||
QHeaderView* headerView = ui->treeView->header();
|
||||
headerView->setSectionResizeMode(QHeaderView::ResizeToContents);
|
||||
headerView->setSectionResizeMode(0, QHeaderView::Stretch);
|
||||
}
|
||||
|
||||
ExportMrPackDialog::~ExportMrPackDialog()
|
||||
{
|
||||
delete ui;
|
||||
}
|
||||
|
||||
void ExportMrPackDialog::done(int result)
|
||||
{
|
||||
if (result == Accepted) {
|
||||
const QString filename = FS::RemoveInvalidFilenameChars(ui->name->text());
|
||||
const QString output = QFileDialog::getSaveFileName(this, tr("Export %1").arg(ui->name->text()),
|
||||
FS::PathCombine(QDir::homePath(), filename + ".mrpack"),
|
||||
"Modrinth pack (*.mrpack *.zip)", nullptr);
|
||||
|
||||
if (output.isEmpty())
|
||||
return;
|
||||
|
||||
ModrinthPackExportTask task(ui->name->text(), ui->version->text(), ui->summary->text(), instance, output,
|
||||
[this](const QString& path) { return proxy->blockedPaths().covers(path); });
|
||||
|
||||
connect(&task, &Task::failed,
|
||||
[this](const QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); });
|
||||
connect(&task, &Task::aborted, [this] {
|
||||
CustomMessageBox::selectable(this, tr("Task aborted"), tr("The task has been aborted by the user."), QMessageBox::Information)
|
||||
->show();
|
||||
});
|
||||
|
||||
ProgressDialog progress(this);
|
||||
progress.setSkipButton(true, tr("Abort"));
|
||||
if (progress.execWithTask(&task) != QDialog::Accepted)
|
||||
return;
|
||||
}
|
||||
|
||||
QDialog::done(result);
|
||||
}
|
||||
|
||||
void ExportMrPackDialog::validate()
|
||||
{
|
||||
const bool invalid = ui->name->text().isEmpty() || ui->version->text().isEmpty();
|
||||
ui->buttonBox->button(QDialogButtonBox::Ok)->setDisabled(invalid);
|
||||
}
|
45
launcher/ui/dialogs/ExportMrPackDialog.h
Normal file
45
launcher/ui/dialogs/ExportMrPackDialog.h
Normal file
@ -0,0 +1,45 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
/*
|
||||
* Prism Launcher - Minecraft Launcher
|
||||
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QDialog>
|
||||
#include "BaseInstance.h"
|
||||
#include "FastFileIconProvider.h"
|
||||
#include "FileIgnoreProxy.h"
|
||||
|
||||
namespace Ui {
|
||||
class ExportMrPackDialog;
|
||||
}
|
||||
|
||||
class ExportMrPackDialog : public QDialog {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ExportMrPackDialog(InstancePtr instance, QWidget* parent = nullptr);
|
||||
~ExportMrPackDialog();
|
||||
|
||||
void done(int result) override;
|
||||
void validate();
|
||||
|
||||
private:
|
||||
const InstancePtr instance;
|
||||
Ui::ExportMrPackDialog* ui;
|
||||
FileIgnoreProxy* proxy;
|
||||
FastFileIconProvider icons;
|
||||
};
|
136
launcher/ui/dialogs/ExportMrPackDialog.ui
Normal file
136
launcher/ui/dialogs/ExportMrPackDialog.ui
Normal file
@ -0,0 +1,136 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>ExportMrPackDialog</class>
|
||||
<widget class="QDialog" name="ExportMrPackDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>650</width>
|
||||
<height>413</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Export Modrinth Pack</string>
|
||||
</property>
|
||||
<property name="sizeGripEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="information">
|
||||
<property name="title">
|
||||
<string>Information</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="versionLabel">
|
||||
<property name="text">
|
||||
<string>Summary</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QLineEdit" name="summary"/>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="nameLabel">
|
||||
<property name="text">
|
||||
<string>Name</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="summaryLabel">
|
||||
<property name="text">
|
||||
<string>Version</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QLineEdit" name="name"/>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QLineEdit" name="version">
|
||||
<property name="text">
|
||||
<string>1.0.0</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="filesLabel">
|
||||
<property name="text">
|
||||
<string>Files</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTreeView" name="treeView">
|
||||
<property name="alternatingRowColors">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::ExtendedSelection</enum>
|
||||
</property>
|
||||
<property name="sortingEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<attribute name="headerStretchLastSection">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<tabstops>
|
||||
<tabstop>name</tabstop>
|
||||
<tabstop>version</tabstop>
|
||||
<tabstop>summary</tabstop>
|
||||
<tabstop>treeView</tabstop>
|
||||
</tabstops>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>ExportMrPackDialog</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>324</x>
|
||||
<y>390</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>324</x>
|
||||
<y>206</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>ExportMrPackDialog</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>324</x>
|
||||
<y>390</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>324</x>
|
||||
<y>206</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
@ -20,14 +20,15 @@
|
||||
#include "ResourceDownloadDialog.h"
|
||||
|
||||
#include <QPushButton>
|
||||
#include <algorithm>
|
||||
|
||||
#include "Application.h"
|
||||
#include "ResourceDownloadTask.h"
|
||||
|
||||
#include "minecraft/mod/ModFolderModel.h"
|
||||
#include "minecraft/mod/ResourcePackFolderModel.h"
|
||||
#include "minecraft/mod/TexturePackFolderModel.h"
|
||||
#include "minecraft/mod/ShaderPackFolderModel.h"
|
||||
#include "minecraft/mod/TexturePackFolderModel.h"
|
||||
|
||||
#include "ui/dialogs/ReviewMessageBox.h"
|
||||
|
||||
@ -41,7 +42,10 @@
|
||||
namespace ResourceDownload {
|
||||
|
||||
ResourceDownloadDialog::ResourceDownloadDialog(QWidget* parent, const std::shared_ptr<ResourceFolderModel> base_model)
|
||||
: QDialog(parent), m_base_model(base_model), m_buttons(QDialogButtonBox::Help | QDialogButtonBox::Ok | QDialogButtonBox::Cancel), m_vertical_layout(this)
|
||||
: QDialog(parent)
|
||||
, m_base_model(base_model)
|
||||
, m_buttons(QDialogButtonBox::Help | QDialogButtonBox::Ok | QDialogButtonBox::Cancel)
|
||||
, m_vertical_layout(this)
|
||||
{
|
||||
setObjectName(QStringLiteral("ResourceDownloadDialog"));
|
||||
|
||||
@ -102,7 +106,8 @@ void ResourceDownloadDialog::initializeContainer()
|
||||
void ResourceDownloadDialog::connectButtons()
|
||||
{
|
||||
auto OkButton = m_buttons.button(QDialogButtonBox::Ok);
|
||||
OkButton->setToolTip(tr("Opens a new popup to review your selected %1 and confirm your selection. Shortcut: Ctrl+Return").arg(resourcesString()));
|
||||
OkButton->setToolTip(
|
||||
tr("Opens a new popup to review your selected %1 and confirm your selection. Shortcut: Ctrl+Return").arg(resourcesString()));
|
||||
connect(OkButton, &QPushButton::clicked, this, &ResourceDownloadDialog::confirm);
|
||||
|
||||
auto CancelButton = m_buttons.button(QDialogButtonBox::Cancel);
|
||||
@ -114,21 +119,24 @@ void ResourceDownloadDialog::connectButtons()
|
||||
|
||||
void ResourceDownloadDialog::confirm()
|
||||
{
|
||||
auto keys = m_selected.keys();
|
||||
keys.sort(Qt::CaseInsensitive);
|
||||
auto selected = getTasks();
|
||||
std::sort(selected.begin(), selected.end(), [](const DownloadTaskPtr& a, const DownloadTaskPtr& b) {
|
||||
return QString::compare(a->getName(), b->getName(), Qt::CaseInsensitive) < 0;
|
||||
});
|
||||
|
||||
auto confirm_dialog = ReviewMessageBox::create(this, tr("Confirm %1 to download").arg(resourcesString()));
|
||||
confirm_dialog->retranslateUi(resourcesString());
|
||||
|
||||
for (auto& task : keys) {
|
||||
auto selected = m_selected.constFind(task).value();
|
||||
confirm_dialog->appendResource({ task, selected->getFilename(), selected->getCustomPath() });
|
||||
for (auto& task : selected) {
|
||||
confirm_dialog->appendResource({ task->getName(), task->getFilename(), task->getCustomPath() });
|
||||
}
|
||||
|
||||
if (confirm_dialog->exec()) {
|
||||
auto deselected = confirm_dialog->deselectedResources();
|
||||
for (auto name : deselected) {
|
||||
m_selected.remove(name);
|
||||
for (auto page : m_container->getPages()) {
|
||||
auto res = static_cast<ResourcePage*>(page);
|
||||
for (auto name : deselected)
|
||||
res->removeResourceFromPage(name);
|
||||
}
|
||||
|
||||
this->accept();
|
||||
@ -145,46 +153,39 @@ ResourcePage* ResourceDownloadDialog::getSelectedPage()
|
||||
return m_selectedPage;
|
||||
}
|
||||
|
||||
void ResourceDownloadDialog::addResource(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, bool is_indexed)
|
||||
void ResourceDownloadDialog::addResource(ModPlatform::IndexedPack::Ptr pack, ModPlatform::IndexedVersion& ver)
|
||||
{
|
||||
removeResource(pack, ver);
|
||||
|
||||
ver.is_currently_selected = true;
|
||||
m_selected.insert(pack.name, makeShared<ResourceDownloadTask>(pack, ver, getBaseModel(), is_indexed));
|
||||
|
||||
m_buttons.button(QDialogButtonBox::Ok)->setEnabled(!m_selected.isEmpty());
|
||||
removeResource(pack->name);
|
||||
m_selectedPage->addResourceToPage(pack, ver, getBaseModel());
|
||||
setButtonStatus();
|
||||
}
|
||||
|
||||
static ModPlatform::IndexedVersion& getVersionWithID(ModPlatform::IndexedPack& pack, QVariant id)
|
||||
void ResourceDownloadDialog::removeResource(const QString& pack_name)
|
||||
{
|
||||
Q_ASSERT(pack.versionsLoaded);
|
||||
auto it = std::find_if(pack.versions.begin(), pack.versions.end(), [id](auto const& v) { return v.fileId == id; });
|
||||
Q_ASSERT(it != pack.versions.end());
|
||||
return *it;
|
||||
}
|
||||
|
||||
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<ResourcePage*>(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<ResourcePage*>(page);
|
||||
selected = selected || res->hasSelectedPacks();
|
||||
}
|
||||
m_buttons.button(QDialogButtonBox::Ok)->setEnabled(selected);
|
||||
}
|
||||
|
||||
const QList<ResourceDownloadDialog::DownloadTaskPtr> ResourceDownloadDialog::getTasks()
|
||||
{
|
||||
return m_selected.values();
|
||||
QList<DownloadTaskPtr> selected;
|
||||
for (auto page : m_container->getPages()) {
|
||||
auto res = static_cast<ResourcePage*>(page);
|
||||
selected.append(res->selectedPacks());
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
void ResourceDownloadDialog::selectedPageChanged(BasePage* previous, BasePage* selected)
|
||||
@ -205,8 +206,6 @@ void ResourceDownloadDialog::selectedPageChanged(BasePage* previous, BasePage* s
|
||||
m_selectedPage->setSearchTerm(prev_page->getSearchTerm());
|
||||
}
|
||||
|
||||
|
||||
|
||||
ModDownloadDialog::ModDownloadDialog(QWidget* parent, const std::shared_ptr<ModFolderModel>& mods, BaseInstance* instance)
|
||||
: ResourceDownloadDialog(parent, mods), m_instance(instance)
|
||||
{
|
||||
@ -232,7 +231,6 @@ QList<BasePage*> ModDownloadDialog::getPages()
|
||||
return pages;
|
||||
}
|
||||
|
||||
|
||||
ResourcePackDownloadDialog::ResourcePackDownloadDialog(QWidget* parent,
|
||||
const std::shared_ptr<ResourcePackFolderModel>& resource_packs,
|
||||
BaseInstance* instance)
|
||||
@ -255,10 +253,11 @@ QList<BasePage*> ResourcePackDownloadDialog::getPages()
|
||||
if (APPLICATION->capabilities() & Application::SupportsFlame)
|
||||
pages.append(FlameResourcePackPage::create(this, *m_instance));
|
||||
|
||||
m_selectedPage = dynamic_cast<ResourcePackResourcePage*>(pages[0]);
|
||||
|
||||
return pages;
|
||||
}
|
||||
|
||||
|
||||
TexturePackDownloadDialog::TexturePackDownloadDialog(QWidget* parent,
|
||||
const std::shared_ptr<TexturePackFolderModel>& resource_packs,
|
||||
BaseInstance* instance)
|
||||
@ -281,10 +280,11 @@ QList<BasePage*> TexturePackDownloadDialog::getPages()
|
||||
if (APPLICATION->capabilities() & Application::SupportsFlame)
|
||||
pages.append(FlameTexturePackPage::create(this, *m_instance));
|
||||
|
||||
m_selectedPage = dynamic_cast<TexturePackResourcePage*>(pages[0]);
|
||||
|
||||
return pages;
|
||||
}
|
||||
|
||||
|
||||
ShaderPackDownloadDialog::ShaderPackDownloadDialog(QWidget* parent,
|
||||
const std::shared_ptr<ShaderPackFolderModel>& shaders,
|
||||
BaseInstance* instance)
|
||||
@ -305,6 +305,8 @@ QList<BasePage*> ShaderPackDownloadDialog::getPages()
|
||||
|
||||
pages.append(ModrinthShaderPackPage::create(this, *m_instance));
|
||||
|
||||
m_selectedPage = dynamic_cast<ShaderPackResourcePage*>(pages[0]);
|
||||
|
||||
return pages;
|
||||
}
|
||||
|
||||
|
@ -62,8 +62,8 @@ class ResourceDownloadDialog : public QDialog, public BasePageProvider {
|
||||
bool selectPage(QString pageId);
|
||||
ResourcePage* getSelectedPage();
|
||||
|
||||
void addResource(ModPlatform::IndexedPack&, ModPlatform::IndexedVersion&, bool is_indexed = false);
|
||||
void removeResource(ModPlatform::IndexedPack&, ModPlatform::IndexedVersion&);
|
||||
void addResource(ModPlatform::IndexedPack::Ptr, ModPlatform::IndexedVersion&);
|
||||
void removeResource(const QString&);
|
||||
|
||||
const QList<DownloadTaskPtr> getTasks();
|
||||
[[nodiscard]] const std::shared_ptr<ResourceFolderModel> getBaseModel() const { return m_base_model; }
|
||||
@ -79,6 +79,7 @@ class ResourceDownloadDialog : public QDialog, public BasePageProvider {
|
||||
|
||||
protected:
|
||||
[[nodiscard]] virtual QString geometrySaveKey() const { return ""; }
|
||||
void setButtonStatus();
|
||||
|
||||
protected:
|
||||
const std::shared_ptr<ResourceFolderModel> m_base_model;
|
||||
@ -88,12 +89,8 @@ class ResourceDownloadDialog : public QDialog, public BasePageProvider {
|
||||
|
||||
QDialogButtonBox m_buttons;
|
||||
QVBoxLayout m_vertical_layout;
|
||||
|
||||
QHash<QString, DownloadTaskPtr> m_selected;
|
||||
};
|
||||
|
||||
|
||||
|
||||
class ModDownloadDialog final : public ResourceDownloadDialog {
|
||||
Q_OBJECT
|
||||
|
||||
@ -135,8 +132,8 @@ class TexturePackDownloadDialog final : public ResourceDownloadDialog {
|
||||
|
||||
public:
|
||||
explicit TexturePackDownloadDialog(QWidget* parent,
|
||||
const std::shared_ptr<TexturePackFolderModel>& resource_packs,
|
||||
BaseInstance* instance);
|
||||
const std::shared_ptr<TexturePackFolderModel>& resource_packs,
|
||||
BaseInstance* instance);
|
||||
~TexturePackDownloadDialog() override = default;
|
||||
|
||||
//: String that gets appended to the texture pack download dialog title ("Download " + resourcesString())
|
||||
@ -153,9 +150,7 @@ class ShaderPackDownloadDialog final : public ResourceDownloadDialog {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ShaderPackDownloadDialog(QWidget* parent,
|
||||
const std::shared_ptr<ShaderPackFolderModel>& shader_packs,
|
||||
BaseInstance* instance);
|
||||
explicit ShaderPackDownloadDialog(QWidget* parent, const std::shared_ptr<ShaderPackFolderModel>& shader_packs, BaseInstance* instance);
|
||||
~ShaderPackDownloadDialog() override = default;
|
||||
|
||||
//: String that gets appended to the shader pack download dialog title ("Download " + resourcesString())
|
||||
|
@ -184,7 +184,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();
|
||||
|
@ -60,15 +60,13 @@ InstanceSettingsPage::InstanceSettingsPage(BaseInstance *inst, QWidget *parent)
|
||||
m_settings = inst->settings();
|
||||
ui->setupUi(this);
|
||||
|
||||
accountMenu = new QMenu(this);
|
||||
// Use undocumented property... https://stackoverflow.com/questions/7121718/create-a-scrollbar-in-a-submenu-qt
|
||||
accountMenu->setStyleSheet("QMenu { menu-scrollable: 1; }");
|
||||
ui->instanceAccountSelector->setMenu(accountMenu);
|
||||
|
||||
connect(ui->openGlobalJavaSettingsButton, &QCommandLinkButton::clicked, this, &InstanceSettingsPage::globalSettingsButtonClicked);
|
||||
connect(APPLICATION, &Application::globalSettingsAboutToOpen, this, &InstanceSettingsPage::applySettings);
|
||||
connect(APPLICATION, &Application::globalSettingsClosed, this, &InstanceSettingsPage::loadSettings);
|
||||
connect(ui->instanceAccountSelector, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &InstanceSettingsPage::changeInstanceAccount);
|
||||
loadSettings();
|
||||
|
||||
|
||||
updateThresholds();
|
||||
}
|
||||
|
||||
@ -454,36 +452,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 +474,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)
|
||||
|
@ -95,12 +95,11 @@ private slots:
|
||||
|
||||
void updateAccountsMenu();
|
||||
QIcon getFaceForAccount(MinecraftAccountPtr account);
|
||||
void changeInstanceAccount();
|
||||
void changeInstanceAccount(int index);
|
||||
|
||||
private:
|
||||
Ui::InstanceSettingsPage *ui;
|
||||
BaseInstance *m_instance;
|
||||
SettingsObjectPtr m_settings;
|
||||
unique_qobject_ptr<JavaCommon::TestCheck> checker;
|
||||
QMenu *accountMenu = nullptr;
|
||||
};
|
||||
|
@ -636,14 +636,7 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QToolButton" name="instanceAccountSelector">
|
||||
<property name="popupMode">
|
||||
<enum>QToolButton::InstantPopup</enum>
|
||||
</property>
|
||||
<property name="toolButtonStyle">
|
||||
<enum>Qt::ToolButtonTextBesideIcon</enum>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QComboBox" name="instanceAccountSelector"/>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
|
@ -55,8 +55,7 @@
|
||||
|
||||
namespace ResourceDownload {
|
||||
|
||||
ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance& instance)
|
||||
: ResourcePage(dialog, instance)
|
||||
ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance& instance) : ResourcePage(dialog, instance)
|
||||
{
|
||||
connect(m_ui->searchButton, &QPushButton::clicked, this, &ModPage::triggerSearch);
|
||||
connect(m_ui->resourceFilterButton, &QPushButton::clicked, this, &ModPage::filterMods);
|
||||
@ -75,12 +74,10 @@ void ModPage::setFilterWidget(unique_qobject_ptr<ModFilterWidget>& widget)
|
||||
m_filter_widget->setInstance(&static_cast<MinecraftInstance&>(m_base_instance));
|
||||
m_filter = m_filter_widget->getFilter();
|
||||
|
||||
connect(m_filter_widget.get(), &ModFilterWidget::filterChanged, this, [&]{
|
||||
m_ui->searchButton->setStyleSheet("text-decoration: underline");
|
||||
});
|
||||
connect(m_filter_widget.get(), &ModFilterWidget::filterUnchanged, this, [&]{
|
||||
m_ui->searchButton->setStyleSheet("text-decoration: none");
|
||||
});
|
||||
connect(m_filter_widget.get(), &ModFilterWidget::filterChanged, this,
|
||||
[&] { m_ui->searchButton->setStyleSheet("text-decoration: underline"); });
|
||||
connect(m_filter_widget.get(), &ModFilterWidget::filterUnchanged, this,
|
||||
[&] { m_ui->searchButton->setStyleSheet("text-decoration: none"); });
|
||||
}
|
||||
|
||||
/******** Callbacks to events in the UI (set up in the derived classes) ********/
|
||||
@ -125,11 +122,11 @@ 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];
|
||||
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 +145,12 @@ void ModPage::updateVersionList()
|
||||
updateSelectionButton();
|
||||
}
|
||||
|
||||
void ModPage::addResourceToDialog(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& version)
|
||||
void ModPage::addResourceToPage(ModPlatform::IndexedPack::Ptr pack,
|
||||
ModPlatform::IndexedVersion& version,
|
||||
const std::shared_ptr<ResourceFolderModel> base_model)
|
||||
{
|
||||
bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool();
|
||||
m_parent_dialog->addResource(pack, version, is_indexed);
|
||||
m_model->addPack(pack, version, base_model, is_indexed);
|
||||
}
|
||||
|
||||
} // namespace ResourceDownload
|
||||
|
@ -8,8 +8,8 @@
|
||||
|
||||
#include "modplatform/ModIndex.h"
|
||||
|
||||
#include "ui/pages/modplatform/ResourcePage.h"
|
||||
#include "ui/pages/modplatform/ModModel.h"
|
||||
#include "ui/pages/modplatform/ResourcePage.h"
|
||||
#include "ui/widgets/ModFilterWidget.h"
|
||||
|
||||
namespace Ui {
|
||||
@ -25,13 +25,14 @@ class ModPage : public ResourcePage {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
template<typename T>
|
||||
template <typename T>
|
||||
static T* create(ModDownloadDialog* dialog, BaseInstance& instance)
|
||||
{
|
||||
auto page = new T(dialog, instance);
|
||||
auto model = static_cast<ModModel*>(page->getModel());
|
||||
|
||||
auto filter_widget = ModFilterWidget::create(static_cast<MinecraftInstance&>(instance).getPackProfile()->getComponentVersion("net.minecraft"), page);
|
||||
auto filter_widget =
|
||||
ModFilterWidget::create(static_cast<MinecraftInstance&>(instance).getPackProfile()->getComponentVersion("net.minecraft"), page);
|
||||
page->setFilterWidget(filter_widget);
|
||||
model->setFilter(page->getFilter());
|
||||
|
||||
@ -48,9 +49,13 @@ class ModPage : public ResourcePage {
|
||||
|
||||
[[nodiscard]] QMap<QString, QString> urlHandlers() const override;
|
||||
|
||||
void addResourceToDialog(ModPlatform::IndexedPack&, ModPlatform::IndexedVersion&) override;
|
||||
void addResourceToPage(ModPlatform::IndexedPack::Ptr,
|
||||
ModPlatform::IndexedVersion&,
|
||||
const std::shared_ptr<ResourceFolderModel>) override;
|
||||
|
||||
virtual auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, std::optional<ResourceAPI::ModLoaderTypes> loaders = {}) const -> bool = 0;
|
||||
virtual auto validateVersion(ModPlatform::IndexedVersion& ver,
|
||||
QString mineVer,
|
||||
std::optional<ResourceAPI::ModLoaderTypes> loaders = {}) const -> bool = 0;
|
||||
|
||||
[[nodiscard]] bool supportsFiltering() const override { return true; };
|
||||
auto getFilter() const -> const std::shared_ptr<ModFilterWidget::Filter> { return m_filter; }
|
||||
|
@ -6,9 +6,11 @@
|
||||
|
||||
#include <QCryptographicHash>
|
||||
#include <QIcon>
|
||||
#include <QList>
|
||||
#include <QMessageBox>
|
||||
#include <QPixmapCache>
|
||||
#include <QUrl>
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
|
||||
#include "Application.h"
|
||||
@ -65,7 +67,7 @@ auto ResourceModel::data(const QModelIndex& index, int role) const -> QVariant
|
||||
return QSize(0, 58);
|
||||
case Qt::UserRole: {
|
||||
QVariant v;
|
||||
v.setValue(*pack);
|
||||
v.setValue(pack);
|
||||
return v;
|
||||
}
|
||||
// Custom data
|
||||
@ -103,7 +105,7 @@ bool ResourceModel::setData(const QModelIndex& index, const QVariant& value, int
|
||||
if (pos >= m_packs.size() || pos < 0 || !index.isValid())
|
||||
return false;
|
||||
|
||||
m_packs[pos] = std::make_shared<ModPlatform::IndexedPack>(value.value<ModPlatform::IndexedPack>());
|
||||
m_packs[pos] = value.value<ModPlatform::IndexedPack::Ptr>();
|
||||
emit dataChanged(index, index);
|
||||
|
||||
return true;
|
||||
@ -230,7 +232,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 +338,15 @@ void ResourceModel::searchRequestSucceeded(QJsonDocument& doc)
|
||||
ModPlatform::IndexedPack::Ptr pack = std::make_shared<ModPlatform::IndexedPack>();
|
||||
try {
|
||||
loadIndexedPack(*pack, packObj);
|
||||
newList.append(pack);
|
||||
if (auto sel = std::find_if(m_selected.begin(), m_selected.end(),
|
||||
[&pack](const DownloadTaskPtr i) {
|
||||
const auto ipack = i->getPack();
|
||||
return ipack->provider == pack->provider && ipack->addonId == pack->addonId;
|
||||
});
|
||||
sel != m_selected.end()) {
|
||||
newList.append(sel->get()->getPack());
|
||||
} else
|
||||
newList.append(pack);
|
||||
} catch (const JSONValidationError& e) {
|
||||
qWarning() << "Error while loading resource from " << debugName() << ": " << e.cause();
|
||||
continue;
|
||||
@ -390,15 +400,15 @@ void ResourceModel::searchRequestAborted()
|
||||
|
||||
void ResourceModel::versionRequestSucceeded(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index)
|
||||
{
|
||||
auto current_pack = data(index, Qt::UserRole).value<ModPlatform::IndexedPack>();
|
||||
auto current_pack = data(index, Qt::UserRole).value<ModPlatform::IndexedPack::Ptr>();
|
||||
|
||||
// Check if the index is still valid for this resource or not
|
||||
if (pack.addonId != current_pack.addonId)
|
||||
if (pack.addonId != current_pack->addonId)
|
||||
return;
|
||||
|
||||
try {
|
||||
auto arr = doc.isObject() ? Json::ensureArray(doc.object(), "data") : doc.array();
|
||||
loadIndexedPackVersions(current_pack, arr);
|
||||
loadIndexedPackVersions(*current_pack, arr);
|
||||
} catch (const JSONValidationError& e) {
|
||||
qDebug() << doc;
|
||||
qWarning() << "Error while reading " << debugName() << " resource version: " << e.cause();
|
||||
@ -417,15 +427,15 @@ void ResourceModel::versionRequestSucceeded(QJsonDocument& doc, ModPlatform::Ind
|
||||
|
||||
void ResourceModel::infoRequestSucceeded(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index)
|
||||
{
|
||||
auto current_pack = data(index, Qt::UserRole).value<ModPlatform::IndexedPack>();
|
||||
auto current_pack = data(index, Qt::UserRole).value<ModPlatform::IndexedPack::Ptr>();
|
||||
|
||||
// Check if the index is still valid for this resource or not
|
||||
if (pack.addonId != current_pack.addonId)
|
||||
if (pack.addonId != current_pack->addonId)
|
||||
return;
|
||||
|
||||
try {
|
||||
auto obj = Json::requireObject(doc);
|
||||
loadExtraPackInfo(current_pack, obj);
|
||||
loadExtraPackInfo(*current_pack, obj);
|
||||
} catch (const JSONValidationError& e) {
|
||||
qDebug() << doc;
|
||||
qWarning() << "Error while reading " << debugName() << " resource info: " << e.cause();
|
||||
@ -442,4 +452,39 @@ void ResourceModel::infoRequestSucceeded(QJsonDocument& doc, ModPlatform::Indexe
|
||||
emit projectInfoUpdated();
|
||||
}
|
||||
|
||||
void ResourceModel::addPack(ModPlatform::IndexedPack::Ptr pack,
|
||||
ModPlatform::IndexedVersion& version,
|
||||
const std::shared_ptr<ResourceFolderModel> packs,
|
||||
bool is_indexed,
|
||||
QString custom_target_folder)
|
||||
{
|
||||
version.is_currently_selected = true;
|
||||
m_selected.append(makeShared<ResourceDownloadTask>(pack, version, packs, is_indexed, custom_target_folder));
|
||||
}
|
||||
|
||||
void ResourceModel::removePack(const QString& rem)
|
||||
{
|
||||
auto pred = [&rem](const DownloadTaskPtr i) { return rem == i->getName(); };
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 1, 0)
|
||||
m_selected.removeIf(pred);
|
||||
#else
|
||||
{
|
||||
for (auto it = m_selected.begin(); it != m_selected.end();)
|
||||
if (pred(*it))
|
||||
it = m_selected.erase(it);
|
||||
else
|
||||
++it;
|
||||
}
|
||||
#endif
|
||||
auto pack = std::find_if(m_packs.begin(), m_packs.end(), [&rem](const ModPlatform::IndexedPack::Ptr i) { return rem == i->name; });
|
||||
if (pack == m_packs.end()) { // ignore it if is not in the current search
|
||||
return;
|
||||
}
|
||||
if (!pack->get()->versionsLoaded) {
|
||||
return;
|
||||
}
|
||||
for (auto& ver : pack->get()->versions)
|
||||
ver.is_currently_selected = false;
|
||||
}
|
||||
|
||||
} // namespace ResourceDownload
|
||||
|
@ -10,6 +10,7 @@
|
||||
|
||||
#include "QObjectPtr.h"
|
||||
|
||||
#include "ResourceDownloadTask.h"
|
||||
#include "modplatform/ResourceAPI.h"
|
||||
|
||||
#include "tasks/ConcurrentTask.h"
|
||||
@ -29,6 +30,8 @@ class ResourceModel : public QAbstractListModel {
|
||||
Q_PROPERTY(QString search_term MEMBER m_search_term WRITE setSearchTerm)
|
||||
|
||||
public:
|
||||
using DownloadTaskPtr = shared_qobject_ptr<ResourceDownloadTask>;
|
||||
|
||||
ResourceModel(ResourceAPI* api);
|
||||
~ResourceModel() override;
|
||||
|
||||
@ -80,6 +83,14 @@ class ResourceModel : public QAbstractListModel {
|
||||
/** Gets the icon at the URL for the given index. If it's not fetched yet, fetch it and update when fisinhed. */
|
||||
std::optional<QIcon> getIcon(QModelIndex&, const QUrl&);
|
||||
|
||||
void addPack(ModPlatform::IndexedPack::Ptr pack,
|
||||
ModPlatform::IndexedVersion& version,
|
||||
const std::shared_ptr<ResourceFolderModel> packs,
|
||||
bool is_indexed = false,
|
||||
QString custom_target_folder = {});
|
||||
void removePack(const QString& rem);
|
||||
QList<DownloadTaskPtr> selectedPacks() { return m_selected; }
|
||||
|
||||
protected:
|
||||
/** Resets the model's data. */
|
||||
void clearData();
|
||||
@ -124,6 +135,7 @@ class ResourceModel : public QAbstractListModel {
|
||||
QSet<QUrl> m_failed_icon_actions;
|
||||
|
||||
QList<ModPlatform::IndexedPack::Ptr> m_packs;
|
||||
QList<DownloadTaskPtr> m_selected;
|
||||
|
||||
// HACK: We need this to prevent callbacks from calling the model after it has already been deleted.
|
||||
// This leaks a tiny bit of memory per time the user has opened a resource dialog. How to make this better?
|
||||
|
@ -37,6 +37,7 @@
|
||||
*/
|
||||
|
||||
#include "ResourcePage.h"
|
||||
#include "modplatform/ModIndex.h"
|
||||
#include "ui_ResourcePage.h"
|
||||
|
||||
#include <QDesktopServices>
|
||||
@ -158,16 +159,16 @@ void ResourcePage::addSortings()
|
||||
m_ui->sortByBox->addItem(sorting.readable_name, QVariant(sorting.index));
|
||||
}
|
||||
|
||||
bool ResourcePage::setCurrentPack(ModPlatform::IndexedPack pack)
|
||||
bool ResourcePage::setCurrentPack(ModPlatform::IndexedPack::Ptr pack)
|
||||
{
|
||||
QVariant v;
|
||||
v.setValue(pack);
|
||||
return m_model->setData(m_ui->packView->currentIndex(), v, Qt::UserRole);
|
||||
}
|
||||
|
||||
ModPlatform::IndexedPack ResourcePage::getCurrentPack() const
|
||||
ModPlatform::IndexedPack::Ptr ResourcePage::getCurrentPack() const
|
||||
{
|
||||
return m_model->data(m_ui->packView->currentIndex(), Qt::UserRole).value<ModPlatform::IndexedPack>();
|
||||
return m_model->data(m_ui->packView->currentIndex(), Qt::UserRole).value<ModPlatform::IndexedPack::Ptr>();
|
||||
}
|
||||
|
||||
void ResourcePage::updateUi()
|
||||
@ -175,14 +176,14 @@ void ResourcePage::updateUi()
|
||||
auto current_pack = getCurrentPack();
|
||||
|
||||
QString text = "";
|
||||
QString name = current_pack.name;
|
||||
QString name = current_pack->name;
|
||||
|
||||
if (current_pack.websiteUrl.isEmpty())
|
||||
if (current_pack->websiteUrl.isEmpty())
|
||||
text = name;
|
||||
else
|
||||
text = "<a href=\"" + current_pack.websiteUrl + "\">" + name + "</a>";
|
||||
text = "<a href=\"" + current_pack->websiteUrl + "\">" + name + "</a>";
|
||||
|
||||
if (!current_pack.authors.empty()) {
|
||||
if (!current_pack->authors.empty()) {
|
||||
auto authorToStr = [](ModPlatform::ModpackAuthor& author) -> QString {
|
||||
if (author.url.isEmpty()) {
|
||||
return author.name;
|
||||
@ -190,44 +191,44 @@ void ResourcePage::updateUi()
|
||||
return QString("<a href=\"%1\">%2</a>").arg(author.url, author.name);
|
||||
};
|
||||
QStringList authorStrs;
|
||||
for (auto& author : current_pack.authors) {
|
||||
for (auto& author : current_pack->authors) {
|
||||
authorStrs.push_back(authorToStr(author));
|
||||
}
|
||||
text += "<br>" + tr(" by ") + authorStrs.join(", ");
|
||||
}
|
||||
|
||||
if (current_pack.extraDataLoaded) {
|
||||
if (!current_pack.extraData.donate.isEmpty()) {
|
||||
if (current_pack->extraDataLoaded) {
|
||||
if (!current_pack->extraData.donate.isEmpty()) {
|
||||
text += "<br><br>" + tr("Donate information: ");
|
||||
auto donateToStr = [](ModPlatform::DonationData& donate) -> QString {
|
||||
return QString("<a href=\"%1\">%2</a>").arg(donate.url, donate.platform);
|
||||
};
|
||||
QStringList donates;
|
||||
for (auto& donate : current_pack.extraData.donate) {
|
||||
for (auto& donate : current_pack->extraData.donate) {
|
||||
donates.append(donateToStr(donate));
|
||||
}
|
||||
text += donates.join(", ");
|
||||
}
|
||||
|
||||
if (!current_pack.extraData.issuesUrl.isEmpty() || !current_pack.extraData.sourceUrl.isEmpty() ||
|
||||
!current_pack.extraData.wikiUrl.isEmpty() || !current_pack.extraData.discordUrl.isEmpty()) {
|
||||
if (!current_pack->extraData.issuesUrl.isEmpty() || !current_pack->extraData.sourceUrl.isEmpty() ||
|
||||
!current_pack->extraData.wikiUrl.isEmpty() || !current_pack->extraData.discordUrl.isEmpty()) {
|
||||
text += "<br><br>" + tr("External links:") + "<br>";
|
||||
}
|
||||
|
||||
if (!current_pack.extraData.issuesUrl.isEmpty())
|
||||
text += "- " + tr("Issues: <a href=%1>%1</a>").arg(current_pack.extraData.issuesUrl) + "<br>";
|
||||
if (!current_pack.extraData.wikiUrl.isEmpty())
|
||||
text += "- " + tr("Wiki: <a href=%1>%1</a>").arg(current_pack.extraData.wikiUrl) + "<br>";
|
||||
if (!current_pack.extraData.sourceUrl.isEmpty())
|
||||
text += "- " + tr("Source code: <a href=%1>%1</a>").arg(current_pack.extraData.sourceUrl) + "<br>";
|
||||
if (!current_pack.extraData.discordUrl.isEmpty())
|
||||
text += "- " + tr("Discord: <a href=%1>%1</a>").arg(current_pack.extraData.discordUrl) + "<br>";
|
||||
if (!current_pack->extraData.issuesUrl.isEmpty())
|
||||
text += "- " + tr("Issues: <a href=%1>%1</a>").arg(current_pack->extraData.issuesUrl) + "<br>";
|
||||
if (!current_pack->extraData.wikiUrl.isEmpty())
|
||||
text += "- " + tr("Wiki: <a href=%1>%1</a>").arg(current_pack->extraData.wikiUrl) + "<br>";
|
||||
if (!current_pack->extraData.sourceUrl.isEmpty())
|
||||
text += "- " + tr("Source code: <a href=%1>%1</a>").arg(current_pack->extraData.sourceUrl) + "<br>";
|
||||
if (!current_pack->extraData.discordUrl.isEmpty())
|
||||
text += "- " + tr("Discord: <a href=%1>%1</a>").arg(current_pack->extraData.discordUrl) + "<br>";
|
||||
}
|
||||
|
||||
text += "<hr>";
|
||||
|
||||
m_ui->packDescription->setHtml(
|
||||
text + (current_pack.extraData.body.isEmpty() ? current_pack.description : markdownToHTML(current_pack.extraData.body)));
|
||||
text + (current_pack->extraData.body.isEmpty() ? current_pack->description : markdownToHTML(current_pack->extraData.body)));
|
||||
m_ui->packDescription->flush();
|
||||
}
|
||||
|
||||
@ -239,7 +240,7 @@ void ResourcePage::updateSelectionButton()
|
||||
}
|
||||
|
||||
m_ui->resourceSelectionButton->setEnabled(true);
|
||||
if (!getCurrentPack().isVersionSelected(m_selected_version_index)) {
|
||||
if (!getCurrentPack()->isVersionSelected(m_selected_version_index)) {
|
||||
m_ui->resourceSelectionButton->setText(tr("Select %1 for download").arg(resourceString()));
|
||||
} else {
|
||||
m_ui->resourceSelectionButton->setText(tr("Deselect %1 for download").arg(resourceString()));
|
||||
@ -254,12 +255,12 @@ 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];
|
||||
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) {
|
||||
@ -279,7 +280,7 @@ void ResourcePage::onSelectionChanged(QModelIndex curr, QModelIndex prev)
|
||||
auto current_pack = getCurrentPack();
|
||||
|
||||
bool request_load = false;
|
||||
if (!current_pack.versionsLoaded) {
|
||||
if (!current_pack->versionsLoaded) {
|
||||
m_ui->resourceSelectionButton->setText(tr("Loading versions..."));
|
||||
m_ui->resourceSelectionButton->setEnabled(false);
|
||||
|
||||
@ -288,7 +289,7 @@ void ResourcePage::onSelectionChanged(QModelIndex curr, QModelIndex prev)
|
||||
updateVersionList();
|
||||
}
|
||||
|
||||
if (!current_pack.extraDataLoaded)
|
||||
if (!current_pack->extraDataLoaded)
|
||||
request_load = true;
|
||||
|
||||
if (request_load)
|
||||
@ -308,14 +309,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<ResourceFolderModel> base_model)
|
||||
{
|
||||
m_model->addPack(pack, ver, base_model);
|
||||
}
|
||||
|
||||
void ResourcePage::removeResourceFromPage(const QString& name)
|
||||
{
|
||||
m_model->removePack(name);
|
||||
}
|
||||
|
||||
void ResourcePage::onResourceSelected()
|
||||
@ -324,12 +337,12 @@ void ResourcePage::onResourceSelected()
|
||||
return;
|
||||
|
||||
auto current_pack = getCurrentPack();
|
||||
if (!current_pack.versionsLoaded)
|
||||
if (!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 +353,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 +383,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 (slug != getCurrentPack()->slug) {
|
||||
m_parent_dialog->selectPage(page);
|
||||
|
||||
auto newPage = m_parent_dialog->getSelectedPage();
|
||||
|
@ -7,10 +7,12 @@
|
||||
#include <QTimer>
|
||||
#include <QWidget>
|
||||
|
||||
#include "ResourceDownloadTask.h"
|
||||
#include "modplatform/ModIndex.h"
|
||||
#include "modplatform/ResourceAPI.h"
|
||||
|
||||
#include "ui/pages/BasePage.h"
|
||||
#include "ui/pages/modplatform/ResourceModel.h"
|
||||
#include "ui/widgets/ProgressWidget.h"
|
||||
|
||||
namespace Ui {
|
||||
@ -27,6 +29,7 @@ class ResourceModel;
|
||||
class ResourcePage : public QWidget, public BasePage {
|
||||
Q_OBJECT
|
||||
public:
|
||||
using DownloadTaskPtr = shared_qobject_ptr<ResourceDownloadTask>;
|
||||
~ResourcePage() override;
|
||||
|
||||
/* Affects what the user sees */
|
||||
@ -57,8 +60,8 @@ class ResourcePage : public QWidget, public BasePage {
|
||||
/** Programatically set the term in the search bar. */
|
||||
void setSearchTerm(QString);
|
||||
|
||||
[[nodiscard]] bool setCurrentPack(ModPlatform::IndexedPack);
|
||||
[[nodiscard]] auto getCurrentPack() const -> ModPlatform::IndexedPack;
|
||||
[[nodiscard]] bool setCurrentPack(ModPlatform::IndexedPack::Ptr);
|
||||
[[nodiscard]] auto getCurrentPack() const -> ModPlatform::IndexedPack::Ptr;
|
||||
[[nodiscard]] auto getDialog() const -> const ResourceDownloadDialog* { return m_parent_dialog; }
|
||||
[[nodiscard]] auto getModel() const -> ResourceModel* { return m_model; }
|
||||
|
||||
@ -72,8 +75,13 @@ class ResourcePage : public QWidget, public BasePage {
|
||||
virtual void updateSelectionButton();
|
||||
virtual void updateVersionList();
|
||||
|
||||
virtual void addResourceToDialog(ModPlatform::IndexedPack&, ModPlatform::IndexedVersion&);
|
||||
virtual void removeResourceFromDialog(ModPlatform::IndexedPack&, ModPlatform::IndexedVersion&);
|
||||
void addResourceToDialog(ModPlatform::IndexedPack::Ptr, ModPlatform::IndexedVersion&);
|
||||
void removeResourceFromDialog(const QString& pack_name);
|
||||
virtual void removeResourceFromPage(const QString& name);
|
||||
virtual void addResourceToPage(ModPlatform::IndexedPack::Ptr, ModPlatform::IndexedVersion&, const std::shared_ptr<ResourceFolderModel>);
|
||||
|
||||
QList<DownloadTaskPtr> selectedPacks() { return m_model->selectedPacks(); }
|
||||
bool hasSelectedPacks() { return !(m_model->selectedPacks().isEmpty()); }
|
||||
|
||||
protected slots:
|
||||
virtual void triggerSearch() {}
|
||||
|
@ -13,8 +13,7 @@
|
||||
|
||||
namespace ResourceDownload {
|
||||
|
||||
ShaderPackResourcePage::ShaderPackResourcePage(ShaderPackDownloadDialog* dialog, BaseInstance& instance)
|
||||
: ResourcePage(dialog, instance)
|
||||
ShaderPackResourcePage::ShaderPackResourcePage(ShaderPackDownloadDialog* dialog, BaseInstance& instance) : ResourcePage(dialog, instance)
|
||||
{
|
||||
connect(m_ui->searchButton, &QPushButton::clicked, this, &ShaderPackResourcePage::triggerSearch);
|
||||
connect(m_ui->packView, &QListView::doubleClicked, this, &ShaderPackResourcePage::onResourceSelected);
|
||||
@ -38,17 +37,20 @@ QMap<QString, QString> ShaderPackResourcePage::urlHandlers() const
|
||||
{
|
||||
QMap<QString, QString> map;
|
||||
map.insert(QRegularExpression::anchoredPattern("(?:www\\.)?modrinth\\.com\\/shaders\\/([^\\/]+)\\/?"), "modrinth");
|
||||
map.insert(QRegularExpression::anchoredPattern("(?:www\\.)?curseforge\\.com\\/minecraft\\/customization\\/([^\\/]+)\\/?"), "curseforge");
|
||||
map.insert(QRegularExpression::anchoredPattern("(?:www\\.)?curseforge\\.com\\/minecraft\\/customization\\/([^\\/]+)\\/?"),
|
||||
"curseforge");
|
||||
map.insert(QRegularExpression::anchoredPattern("minecraft\\.curseforge\\.com\\/projects\\/([^\\/]+)\\/?"), "curseforge");
|
||||
return map;
|
||||
}
|
||||
|
||||
void ShaderPackResourcePage::addResourceToDialog(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& version)
|
||||
void ShaderPackResourcePage::addResourceToPage(ModPlatform::IndexedPack::Ptr pack,
|
||||
ModPlatform::IndexedVersion& version,
|
||||
const std::shared_ptr<ResourceFolderModel> base_model)
|
||||
{
|
||||
QString custom_target_folder;
|
||||
if (version.loaders.contains(QStringLiteral("canvas")))
|
||||
version.custom_target_folder = QStringLiteral("resourcepacks");
|
||||
|
||||
m_parent_dialog->addResource(pack, version);
|
||||
custom_target_folder = QStringLiteral("resourcepacks");
|
||||
m_model->addPack(pack, version, base_model, false, custom_target_folder);
|
||||
}
|
||||
|
||||
} // namespace ResourceDownload
|
||||
|
@ -38,7 +38,9 @@ class ShaderPackResourcePage : public ResourcePage {
|
||||
|
||||
[[nodiscard]] bool supportsFiltering() const override { return false; };
|
||||
|
||||
void addResourceToDialog(ModPlatform::IndexedPack&, ModPlatform::IndexedVersion&) override;
|
||||
void addResourceToPage(ModPlatform::IndexedPack::Ptr,
|
||||
ModPlatform::IndexedVersion&,
|
||||
const std::shared_ptr<ResourceFolderModel>) override;
|
||||
|
||||
[[nodiscard]] QMap<QString, QString> urlHandlers() const override;
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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();
|
||||
|
@ -137,6 +137,11 @@ BasePage* PageContainer::getPage(QString pageId)
|
||||
return m_model->findPageEntryById(pageId);
|
||||
}
|
||||
|
||||
const QList<BasePage*> PageContainer::getPages() const
|
||||
{
|
||||
return m_model->pages();
|
||||
}
|
||||
|
||||
void PageContainer::refreshContainer()
|
||||
{
|
||||
m_proxyModel->invalidate();
|
||||
|
@ -80,6 +80,7 @@ public:
|
||||
|
||||
virtual bool selectPage(QString pageId) override;
|
||||
BasePage* getPage(QString pageId) override;
|
||||
const QList<BasePage*> getPages() const;
|
||||
|
||||
void refreshContainer() override;
|
||||
virtual void setParentContainer(BasePageContainer * container)
|
||||
|
120
nix/default.nix
120
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 = "pollymc";
|
||||
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://github.com/fn2006/PollyMC";
|
||||
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/fn2006/PollyMC/releases/tag/${version}";
|
||||
license = licenses.gpl3Only;
|
||||
maintainers = with maintainers; [ fn2006 ];
|
||||
_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"
|
||||
];
|
||||
}
|
||||
|
46
nix/dev.nix
Normal file
46
nix/dev.nix
Normal file
@ -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}.default];
|
||||
buildInputs = with pkgs; [ccache ninja];
|
||||
};
|
||||
|
||||
formatter = pkgs.alejandra;
|
||||
};
|
||||
}
|
29
nix/distribution.nix
Normal file
29
nix/distribution.nix
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
inputs,
|
||||
self,
|
||||
version,
|
||||
...
|
||||
}: {
|
||||
perSystem = {pkgs, ...}: {
|
||||
packages = {
|
||||
inherit (pkgs) pollymc-qt5-unwrapped pollymc-qt5 pollymc-unwrapped pollymc;
|
||||
default = pkgs.pollymc;
|
||||
};
|
||||
};
|
||||
|
||||
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 {
|
||||
pollymc-qt5-unwrapped = mkPrism final.libsForQt5;
|
||||
pollymc-qt5 = prev.pollymc-qt5.override {pollymc-unwrapped = final.pollymc-qt5-unwrapped;};
|
||||
pollymc-unwrapped = mkPrism final.qt6Packages;
|
||||
pollymc = prev.pollymc.override {inherit (final) pollymc-unwrapped;};
|
||||
};
|
||||
};
|
||||
}
|
@ -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 = ../.;}
|
65
nix/package.nix
Normal file
65
nix/package.nix
Normal file
@ -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 = "pollymc-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://github.com/fn2006/PollyMC";
|
||||
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/fn2006/PollyMC/releases/tag/${version}";
|
||||
license = licenses.gpl3Only;
|
||||
maintainers = with maintainers; [fn2006];
|
||||
};
|
||||
}
|
@ -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@"
|
||||
|
@ -2,5 +2,11 @@
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:base"
|
||||
]
|
||||
],
|
||||
"nix": {
|
||||
"enabled": true
|
||||
},
|
||||
"lockFileMaintenance": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user