Merge branch 'PrismLauncher:develop' into develop

This commit is contained in:
Ash 2023-07-28 21:22:35 +00:00 committed by GitHub
commit d133b40b25
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
161 changed files with 4266 additions and 2032 deletions

8
.editorconfig Normal file
View File

@ -0,0 +1,8 @@
# EditorConfig specs and documentation: https://EditorConfig.org
# top-most EditorConfig file
root = true
# C++ Code Style settings
[*.{c++,cc,cpp,cppm,cxx,h,h++,hh,hpp,hxx,inl,ipp,ixx,tlh,tli}]
cpp_generate_documentation_comments = doxygen_slash_star

32
.github/workflows/backport.yml vendored Normal file
View File

@ -0,0 +1,32 @@
name: Backport
on:
pull_request_target:
types: [closed, labeled]
# WARNING:
# When extending this action, be aware that $GITHUB_TOKEN allows write access to
# the GitHub repository. This means that it should not evaluate user input in a
# way that allows code injection.
permissions:
contents: read
jobs:
backport:
permissions:
contents: write # for korthout/backport-action to create branch
pull-requests: write # for korthout/backport-action to create PR to backport
name: Backport Pull Request
if: github.repository_owner == 'PrismLauncher' && github.event.pull_request.merged == true && (github.event_name != 'labeled' || startsWith('backport', github.event.label.name))
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Create backport PRs
uses: korthout/backport-action@v1.3.1
with:
# Config README: https://github.com/korthout/backport-action#backport-action
pull_description: |-
Bot-based backport to `${target_branch}`, triggered by a label in #${pull_number}.

View File

@ -264,23 +264,23 @@ jobs:
- name: Configure CMake (macOS) - name: Configure CMake (macOS)
if: runner.os == 'macOS' && matrix.qt_ver == 6 if: runner.os == 'macOS' && matrix.qt_ver == 6
run: | run: |
cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=${{ matrix.name }} -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DCMAKE_OSX_ARCHITECTURES="x86_64;arm64" -G Ninja cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=official -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DCMAKE_OSX_ARCHITECTURES="x86_64;arm64" -G Ninja
- name: Configure CMake (macOS-Legacy) - name: Configure CMake (macOS-Legacy)
if: runner.os == 'macOS' && matrix.qt_ver == 5 if: runner.os == 'macOS' && matrix.qt_ver == 5
run: | run: |
cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=${{ matrix.name }} -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DMACOSX_SPARKLE_UPDATE_PUBLIC_KEY="" -DMACOSX_SPARKLE_UPDATE_FEED_URL="" -G Ninja cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=official -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DMACOSX_SPARKLE_UPDATE_PUBLIC_KEY="" -DMACOSX_SPARKLE_UPDATE_FEED_URL="" -G Ninja
- name: Configure CMake (Windows MinGW-w64) - name: Configure CMake (Windows MinGW-w64)
if: runner.os == 'Windows' && matrix.msystem != '' if: runner.os == 'Windows' && matrix.msystem != ''
shell: msys2 {0} shell: msys2 {0}
run: | run: |
cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=${{ matrix.name }} -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=6 -DCMAKE_OBJDUMP=/mingw64/bin/objdump.exe -G Ninja cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=official -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=6 -DCMAKE_OBJDUMP=/mingw64/bin/objdump.exe -G Ninja
- name: Configure CMake (Windows MSVC) - name: Configure CMake (Windows MSVC)
if: runner.os == 'Windows' && matrix.msystem == '' if: runner.os == 'Windows' && matrix.msystem == ''
run: | run: |
cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=${{ matrix.name }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DCMAKE_MSVC_RUNTIME_LIBRARY="MultiThreadedDLL" -A${{ matrix.architecture}} -DLauncher_FORCE_BUNDLED_LIBS=ON cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=official -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DCMAKE_MSVC_RUNTIME_LIBRARY="MultiThreadedDLL" -A${{ matrix.architecture}} -DLauncher_FORCE_BUNDLED_LIBS=ON
# https://github.com/ccache/ccache/wiki/MS-Visual-Studio (I coudn't figure out the compiler prefix) # https://github.com/ccache/ccache/wiki/MS-Visual-Studio (I coudn't figure out the compiler prefix)
if ("${{ env.CCACHE_VAR }}") if ("${{ env.CCACHE_VAR }}")
{ {
@ -295,7 +295,7 @@ jobs:
- name: Configure CMake (Linux) - name: Configure CMake (Linux)
if: runner.os == 'Linux' if: runner.os == 'Linux'
run: | run: |
cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=Linux -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -G Ninja cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=official -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -G Ninja
## ##
# BUILD # BUILD
@ -611,33 +611,3 @@ jobs:
with: with:
bundle: "Prism Launcher.flatpak" bundle: "Prism Launcher.flatpak"
manifest-path: flatpak/org.prismlauncher.PrismLauncher.yml manifest-path: flatpak/org.prismlauncher.PrismLauncher.yml
nix:
runs-on: ubuntu-latest
strategy:
matrix:
package:
- prismlauncher
- prismlauncher-qt5
steps:
- name: Clone repository
if: inputs.build_type == 'Debug'
uses: actions/checkout@v3
with:
submodules: 'true'
- name: Install nix
if: inputs.build_type == 'Debug'
uses: cachix/install-nix-action@v22
with:
install_url: https://nixos.org/nix/install
extra_nix_config: |
auto-optimise-store = true
experimental-features = nix-command flakes
- uses: cachix/cachix-action@v12
if: inputs.build_type == 'Debug'
with:
name: prismlauncher
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
- name: Build
if: inputs.build_type == 'Debug'
run: nix build .#${{ matrix.package }} --print-build-logs

28
.github/workflows/update-flake.yml vendored Normal file
View File

@ -0,0 +1,28 @@
name: Update Flake Lockfile
on:
schedule:
# run weekly on sunday
- cron: "0 0 * * 0"
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
update-flake:
if: github.repository == 'PrismLauncher/PrismLauncher'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: cachix/install-nix-action@v22
- uses: DeterminateSystems/update-flake-lock@v19
with:
commit-msg: "chore(nix): update lockfile"
pr-title: "chore(nix): update lockfile"
pr-labels: |
Linux
simple change

3
.gitmodules vendored
View File

@ -19,3 +19,6 @@
[submodule "libraries/cmark"] [submodule "libraries/cmark"]
path = libraries/cmark path = libraries/cmark
url = https://github.com/commonmark/cmark.git url = https://github.com/commonmark/cmark.git
[submodule "flatpak/shared-modules"]
path = flatpak/shared-modules
url = https://github.com/flathub/shared-modules.git

View File

@ -85,6 +85,38 @@ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DTOML_ENABLE_FLOAT16=0")
# set CXXFLAGS for build targets # set CXXFLAGS for build targets
set(CMAKE_CXX_FLAGS_RELEASE "-O2 -D_FORTIFY_SOURCE=2 ${CMAKE_CXX_FLAGS_RELEASE}") set(CMAKE_CXX_FLAGS_RELEASE "-O2 -D_FORTIFY_SOURCE=2 ${CMAKE_CXX_FLAGS_RELEASE}")
option(DEBUG_ADDRESS_SANITIZER "Enable Address Sanitizer in Debug builds" on)
# If this is a Debug build turn on address sanitiser
if (CMAKE_BUILD_TYPE STREQUAL "Debug" AND DEBUG_ADDRESS_SANITIZER)
message(STATUS "Address Sanitizer enabled for Debug builds, Turn it off with -DDEBUG_ADDRESS_SANITIZER=off")
if ("${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang")
if (CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC")
# using clang with clang-cl front end
message(STATUS "Address Sanitizer available on Clang MSVC frontend")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /fsanitize=address /O1 /Oy-")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /fsanitize=address /O1 /Oy-")
else()
# AppleClang and Clang
message(STATUS "Address Sanitizer available on Clang")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -O1 -fno-omit-frame-pointer")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address -O1 -fno-omit-frame-pointer")
endif()
elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU")
# GCC
message(STATUS "Address Sanitizer available on GCC")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -O1 -fno-omit-frame-pointer")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address -O1 -fno-omit-frame-pointer")
link_libraries("asan")
elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "MSVC")
message(STATUS "Address Sanitizer available on MSVC")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /fsanitize=address /O1 /Oy-")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /fsanitize=address /O1 /Oy-")
else()
message(STATUS "Address Sanitizer not available on compiler ${CMAKE_CXX_COMPILER_ID}")
endif()
endif()
option(ENABLE_LTO "Enable Link Time Optimization" off) option(ENABLE_LTO "Enable Link Time Optimization" off)
if(ENABLE_LTO) if(ENABLE_LTO)
@ -146,7 +178,7 @@ set(Launcher_VERSION_NAME4 "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.
set(Launcher_VERSION_NAME4_COMMA "${Launcher_VERSION_MAJOR},${Launcher_VERSION_MINOR},0,0") set(Launcher_VERSION_NAME4_COMMA "${Launcher_VERSION_MAJOR},${Launcher_VERSION_MINOR},0,0")
# Build platform. # Build platform.
set(Launcher_BUILD_PLATFORM "" CACHE STRING "A short string identifying the platform that this build was built for. Only used to display in the about dialog.") set(Launcher_BUILD_PLATFORM "unknown" CACHE STRING "A short string identifying the platform that this build was built for. Only used to display in the about dialog.")
# Channel list URL # Channel list URL
set(Launcher_UPDATER_BASE "" CACHE STRING "Base URL for the updater.") set(Launcher_UPDATER_BASE "" CACHE STRING "Base URL for the updater.")
@ -332,7 +364,7 @@ elseif(UNIX)
set(BINARY_DEST_DIR "bin") set(BINARY_DEST_DIR "bin")
set(LIBRARY_DEST_DIR "lib${LIB_SUFFIX}") set(LIBRARY_DEST_DIR "lib${LIB_SUFFIX}")
set(JARS_DEST_DIR "share/${Launcher_APP_BINARY_NAME}") set(JARS_DEST_DIR "share/${Launcher_Name}")
# install as bundle with no dependencies included # install as bundle with no dependencies included
set(INSTALL_BUNDLE "nodeps") set(INSTALL_BUNDLE "nodeps")
@ -345,7 +377,7 @@ elseif(UNIX)
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_SVG} DESTINATION "${KDE_INSTALL_ICONDIR}/hicolor/scalable/apps") install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_SVG} DESTINATION "${KDE_INSTALL_ICONDIR}/hicolor/scalable/apps")
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_mrpack_MIMEInfo} DESTINATION ${KDE_INSTALL_MIMEDIR}) install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_mrpack_MIMEInfo} DESTINATION ${KDE_INSTALL_MIMEDIR})
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/launcher/qtlogging.ini" DESTINATION "${KDE_INSTALL_DATADIR}/${Launcher_Name}") install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/launcher/qtlogging.ini" DESTINATION "share/${Launcher_Name}")
if(Launcher_ManPage) if(Launcher_ManPage)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_ManPage} DESTINATION "${KDE_INSTALL_MANDIR}/man6") install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_ManPage} DESTINATION "${KDE_INSTALL_MANDIR}/man6")

View File

@ -19,7 +19,7 @@ In an effort to ensure that the code you contribute is actually compatible with
This can be done by appending `-s` to your `git commit` call, or by manually appending the following text to your commit message: This can be done by appending `-s` to your `git commit` call, or by manually appending the following text to your commit message:
``` ```text
<commit message> <commit message>
Signed-off-by: Author name <Author email> Signed-off-by: Author name <Author email>
@ -27,7 +27,7 @@ Signed-off-by: Author name <Author email>
By signing off your work, you agree to the terms below: By signing off your work, you agree to the terms below:
``` ```text
Developer's Certificate of Origin 1.1 Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that: By making a contribution to this project, I certify that:
@ -61,3 +61,9 @@ As a bonus, you can also [cryptographically sign your commits][gh-signing-commit
[gh-signing-commits]: https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits [gh-signing-commits]: https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits
[gh-vigilant-mode]: https://docs.github.com/en/authentication/managing-commit-signature-verification/displaying-verification-statuses-for-all-of-your-commits [gh-vigilant-mode]: https://docs.github.com/en/authentication/managing-commit-signature-verification/displaying-verification-statuses-for-all-of-your-commits
## Backporting to Release Branches
We use [automated backports](https://github.com/PrismLauncher/PrismLauncher/blob/develop/.github/workflows/backport.yml) to merge specific contributions from develop into `release` branches.
This is done when pull requests are merged and have labels such as `backport release-7.x` - which should be added along with the milestone for the release.

View File

@ -82,14 +82,16 @@ Thanks to the awesome people over at [MacStadium](https://www.macstadium.com/),
## Forking/Redistributing/Custom builds policy ## Forking/Redistributing/Custom builds policy
We don't care what you do with your fork/custom build as long as you follow the terms of the [license](LICENSE) (this is a legal responsibility), and if you made code changes rather than just packaging a custom build, please do the following as a basic courtesy: You are free to fork, redistribute and provide custom builds as long as you follow the terms of the [license](LICENSE) (this is a legal responsibility), and if you made code changes rather than just packaging a custom build, please do the following as a basic courtesy:
- Make it clear that your fork is not Prism Launcher and is not endorsed by or affiliated with the Prism Launcher project (<https://prismlauncher.org>). - Make it clear that your fork is not Prism Launcher and is not endorsed by or affiliated with the Prism Launcher project (<https://prismlauncher.org>).
- Go through [CMakeLists.txt](CMakeLists.txt) and change Prism Launcher's API keys to your own or set them to empty strings (`""`) to disable them (this way the program will still compile but the functionality requiring those keys will be disabled). - Go through [CMakeLists.txt](CMakeLists.txt) and change Prism Launcher's API keys to your own or set them to empty strings (`""`) to disable them (this way the program will still compile but the functionality requiring those keys will be disabled).
If you have any questions or want any clarification on the above conditions please make an issue and ask us. If you have any questions or want any clarification on the above conditions please make an issue and ask us.
Be aware that if you build this software without removing the provided API keys in [CMakeLists.txt](CMakeLists.txt) you are accepting the following terms and conditions: If you are just building Prism Launcher for your distribution, please make sure to set the `Launcher_BUILD_PLATFORM` to a slug representing your distribution. Examples are `archlinux`, `fedora` and `nixpkgs`.
Note that if you build this software without removing the provided API keys in [CMakeLists.txt](CMakeLists.txt) you are accepting the following terms and conditions:
- [Microsoft Identity Platform Terms of Use](https://docs.microsoft.com/en-us/legal/microsoft-identity-platform/terms-of-use) - [Microsoft Identity Platform Terms of Use](https://docs.microsoft.com/en-us/legal/microsoft-identity-platform/terms-of-use)
- [CurseForge 3rd Party API Terms and Conditions](https://support.curseforge.com/en/support/solutions/articles/9000207405-curse-forge-3rd-party-api-terms-and-conditions) - [CurseForge 3rd Party API Terms and Conditions](https://support.curseforge.com/en/support/solutions/articles/9000207405-curse-forge-3rd-party-api-terms-and-conditions)

View File

@ -65,7 +65,7 @@ Config::Config()
MAC_SPARKLE_PUB_KEY = "@MACOSX_SPARKLE_UPDATE_PUBLIC_KEY@"; MAC_SPARKLE_PUB_KEY = "@MACOSX_SPARKLE_UPDATE_PUBLIC_KEY@";
MAC_SPARKLE_APPCAST_URL = "@MACOSX_SPARKLE_UPDATE_FEED_URL@"; MAC_SPARKLE_APPCAST_URL = "@MACOSX_SPARKLE_UPDATE_FEED_URL@";
if (BUILD_PLATFORM == "macOS" && !MAC_SPARKLE_PUB_KEY.isEmpty() && !MAC_SPARKLE_APPCAST_URL.isEmpty()) if (!MAC_SPARKLE_PUB_KEY.isEmpty() && !MAC_SPARKLE_APPCAST_URL.isEmpty())
{ {
UPDATER_ENABLED = true; UPDATER_ENABLED = true;
} }

View File

@ -68,7 +68,7 @@ class Config {
bool UPDATER_ENABLED = false; bool UPDATER_ENABLED = false;
/// A short string identifying this build's platform. For example, "lin64" or "win32". /// A short string identifying this build's platform or distribution.
QString BUILD_PLATFORM; QString BUILD_PLATFORM;
/// A string containing the build timestamp /// A string containing the build timestamp

54
flake.lock generated
View File

@ -21,11 +21,11 @@
"nixpkgs-lib": "nixpkgs-lib" "nixpkgs-lib": "nixpkgs-lib"
}, },
"locked": { "locked": {
"lastModified": 1683560683, "lastModified": 1688466019,
"narHash": "sha256-XAygPMN5Xnk/W2c1aW0jyEa6lfMDZWlQgiNtmHXytPc=", "narHash": "sha256-VeM2akYrBYMsb4W/MmBo1zmaMfgbL4cH3Pu8PGyIwJ0=",
"owner": "hercules-ci", "owner": "hercules-ci",
"repo": "flake-parts", "repo": "flake-parts",
"rev": "006c75898cf814ef9497252b022e91c946ba8e17", "rev": "8e8d955c22df93dbe24f19ea04f47a74adbdc5ec",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -35,12 +35,15 @@
} }
}, },
"flake-utils": { "flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": { "locked": {
"lastModified": 1667395993, "lastModified": 1685518550,
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=", "narHash": "sha256-o2d0KcvaXzTrPRIo0kOLV0/QXHhDQ5DTi+OxcjO8xqY=",
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "flake-utils",
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f", "rev": "a1720a10a6cfe8234c0e93907ffe81be440f4cef",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -73,11 +76,11 @@
"libnbtplusplus": { "libnbtplusplus": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1650031308, "lastModified": 1690036783,
"narHash": "sha256-TvVOjkUobYJD9itQYueELJX3wmecvEdCbJ0FinW2mL4=", "narHash": "sha256-A5kTgICnx+Qdq3Fir/bKTfdTt/T1NQP2SC+nhN1ENug=",
"owner": "PrismLauncher", "owner": "PrismLauncher",
"repo": "libnbtplusplus", "repo": "libnbtplusplus",
"rev": "2203af7eeb48c45398139b583615134efd8d407f", "rev": "a5e8fd52b8bf4ab5d5bcc042b2a247867589985f",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -88,11 +91,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1685012353, "lastModified": 1690026219,
"narHash": "sha256-U3oOge4cHnav8OLGdRVhL45xoRj4Ppd+It6nPC9nNIU=", "narHash": "sha256-oOduRk/kzQxOBknZXTLSEYd7tk+GoKvr8wV6Ab+t4AU=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "aeb75dba965e790de427b73315d5addf91a54955", "rev": "f465da166263bc0d4b39dfd4ca28b777c92d4b73",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -105,11 +108,11 @@
"nixpkgs-lib": { "nixpkgs-lib": {
"locked": { "locked": {
"dir": "lib", "dir": "lib",
"lastModified": 1682879489, "lastModified": 1688049487,
"narHash": "sha256-sASwo8gBt7JDnOOstnps90K1wxmVfyhsTPPNTGBPjjg=", "narHash": "sha256-100g4iaKC9MalDjUW9iN6Jl/OocTDtXdeAj7pEGIRh4=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "da45bf6ec7bbcc5d1e14d3795c025199f28e0de0", "rev": "4bc72cae107788bf3f24f30db2e2f685c9298dc9",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -135,11 +138,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1684842236, "lastModified": 1689668210,
"narHash": "sha256-rYWsIXHvNhVQ15RQlBUv67W3YnM+Pd+DuXGMvCBq2IE=", "narHash": "sha256-XAATwDkaUxH958yXLs1lcEOmU6pSEIkatY3qjqk8X0E=",
"owner": "cachix", "owner": "cachix",
"repo": "pre-commit-hooks.nix", "repo": "pre-commit-hooks.nix",
"rev": "61e567d6497bc9556f391faebe5e410e6623217f", "rev": "eb433bff05b285258be76513add6f6c57b441775",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -156,6 +159,21 @@
"nixpkgs": "nixpkgs", "nixpkgs": "nixpkgs",
"pre-commit-hooks": "pre-commit-hooks" "pre-commit-hooks": "pre-commit-hooks"
} }
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
} }
}, },
"root": "root", "root": "root",

22
flatpak/libdecor.json Normal file
View File

@ -0,0 +1,22 @@
{
"name": "libdecor",
"buildsystem": "meson",
"config-opts": [
"-Ddemo=false"
],
"sources": [
{
"type": "git",
"url": "https://gitlab.freedesktop.org/libdecor/libdecor.git",
"commit": "73260393a97291c887e1074ab7f318e031be0ac6"
},
{
"type": "patch",
"path": "patches/weird_libdecor.patch"
}
],
"cleanup": [
"/include",
"/lib/pkgconfig"
]
}

View File

@ -5,13 +5,6 @@ sdk: org.kde.Sdk
sdk-extensions: sdk-extensions:
- org.freedesktop.Sdk.Extension.openjdk17 - org.freedesktop.Sdk.Extension.openjdk17
- org.freedesktop.Sdk.Extension.openjdk8 - org.freedesktop.Sdk.Extension.openjdk8
add-extensions:
com.valvesoftware.Steam.Utility.gamescope:
version: stable
add-ld-path: lib
no-autodownload: true
autodelete: false
directory: utils/gamescope
command: prismlauncher command: prismlauncher
finish-args: finish-args:
@ -26,12 +19,22 @@ finish-args:
# Mod drag&drop # Mod drag&drop
- --filesystem=xdg-download:ro - --filesystem=xdg-download:ro
cleanup:
- /lib/libGLU*
modules: modules:
# Might be needed by some Controller mods (see https://github.com/isXander/Controlify/issues/31)
- shared-modules/libusb/libusb.json
# Needed for proper Wayland support
- libdecor.json
- name: prismlauncher - name: prismlauncher
buildsystem: cmake-ninja buildsystem: cmake-ninja
builddir: true
config-opts: config-opts:
- -DLauncher_BUILD_PLATFORM=flatpak - -DLauncher_BUILD_PLATFORM=flatpak
- -DCMAKE_BUILD_TYPE=Debug - -DCMAKE_BUILD_TYPE=RelWithDebInfo
- -DLauncher_QT_VERSION_MAJOR=5 - -DLauncher_QT_VERSION_MAJOR=5
build-options: build-options:
env: env:
@ -40,7 +43,7 @@ modules:
sources: sources:
- type: dir - type: dir
path: ../ path: ../
builddir: true
- name: openjdk - name: openjdk
buildsystem: simple buildsystem: simple
build-commands: build-commands:
@ -49,14 +52,45 @@ modules:
- mv /app/jre /app/jdk/17 - mv /app/jre /app/jdk/17
- /usr/lib/sdk/openjdk8/install.sh - /usr/lib/sdk/openjdk8/install.sh
- mv /app/jre /app/jdk/8 - mv /app/jre /app/jdk/8
cleanup: [/jre] cleanup:
- /jre
- name: glfw
buildsystem: cmake-ninja
config-opts:
- -DCMAKE_BUILD_TYPE=RelWithDebInfo
- -DBUILD_SHARED_LIBS:BOOL=ON
- -DGLFW_USE_WAYLAND=ON
sources:
- type: git
url: https://github.com/glfw/glfw.git
commit: 3fa2360720eeba1964df3c0ecf4b5df8648a8e52
- type: patch
path: patches/0003-Don-t-crash-on-calls-to-focus-or-icon.patch
- type: patch
path: patches/0005-Add-warning-about-being-an-unofficial-patch.patch
- type: patch
path: patches/0007-Platform-Prefer-Wayland-over-X11.patch
cleanup:
- /include
- /lib/cmake
- /lib/pkgconfig
- name: xrandr - name: xrandr
buildsystem: autotools buildsystem: autotools
sources: sources:
- type: archive - type: archive
url: https://xorg.freedesktop.org/archive/individual/app/xrandr-1.5.1.tar.xz url: https://xorg.freedesktop.org/archive/individual/app/xrandr-1.5.2.tar.xz
sha256: 7bc76daf9d72f8aff885efad04ce06b90488a1a169d118dea8a2b661832e8762 sha256: c8bee4790d9058bacc4b6246456c58021db58a87ddda1a9d0139bf5f18f1f240
cleanup: [/share/man, /bin/xkeystone] x-checker-data:
type: anitya
project-id: 14957
stable-only: true
url-template: https://xorg.freedesktop.org/archive/individual/app/xrandr-$version.tar.xz
cleanup:
- /share/man
- /bin/xkeystone
- name: gamemode - name: gamemode
buildsystem: meson buildsystem: meson
config-opts: config-opts:
@ -67,19 +101,56 @@ modules:
# post-install is running inside the build dir, we need it from the source though # post-install is running inside the build dir, we need it from the source though
- install -Dm755 ../data/gamemoderun -t /app/bin - install -Dm755 ../data/gamemoderun -t /app/bin
sources: sources:
- type: git - type: archive
url: https://github.com/FeralInteractive/gamemode archive-type: tar-gzip
tag: "1.7" url: https://api.github.com/repos/FeralInteractive/gamemode/tarball/1.7
commit: 4dc99dff76218718763a6b07fc1900fa6d1dafd9 sha256: 57ce73ba605d1cf12f8d13725006a895182308d93eba0f69f285648449641803
x-checker-data:
type: json
url: https://api.github.com/repos/FeralInteractive/gamemode/releases/latest
version-query: .tag_name
url-query: .tarball_url
timestamp-query: .published_at
cleanup:
- /include
- /lib/pkgconfig
- /lib/libgamemodeauto.a
- name: glxinfo
buildsystem: meson
config-opts:
- --bindir=/app/mesa-demos
- -Degl=disabled
- -Dglut=disabled
- -Dosmesa=disabled
- -Dvulkan=disabled
- -Dwayland=disabled
post-install:
- mv -v /app/mesa-demos/glxinfo /app/bin
sources:
- type: archive
url: https://archive.mesa3d.org/demos/mesa-demos-9.0.0.tar.xz
sha256: 3046a3d26a7b051af7ebdd257a5f23bfeb160cad6ed952329cdff1e9f1ed496b
x-checker-data:
type: anitya
project-id: 16781
stable-only: true
url-template: https://archive.mesa3d.org/demos/mesa-demos-$version.tar.xz
cleanup:
- /include
- /mesa-demos
- /share
modules:
- shared-modules/glu/glu-9.json
- name: enhance - name: enhance
buildsystem: simple buildsystem: simple
build-commands: build-commands:
- mkdir -p /app/utils/gamescope
- install -Dm755 prime-run /app/bin/prime-run - install -Dm755 prime-run /app/bin/prime-run
- mv /app/bin/prismlauncher /app/bin/prismrun - mv /app/bin/prismlauncher /app/bin/prismrun
- install -Dm755 prismlauncher /app/bin/prismlauncher - install -Dm755 prismlauncher /app/bin/prismlauncher
sources: sources:
- type: file - type: file
path: ../flatpak/prime-run path: prime-run
- type: file - type: file
path: ../flatpak/prismlauncher path: prismlauncher

View File

@ -0,0 +1,24 @@
diff --git a/src/wl_window.c b/src/wl_window.c
index 52d3b9eb..4ac4eb5d 100644
--- a/src/wl_window.c
+++ b/src/wl_window.c
@@ -2117,8 +2117,7 @@ void _glfwSetWindowTitleWayland(_GLFWwindow* window, const char* title)
void _glfwSetWindowIconWayland(_GLFWwindow* window,
int count, const GLFWimage* images)
{
- _glfwInputError(GLFW_FEATURE_UNAVAILABLE,
- "Wayland: The platform does not support setting the window icon");
+ fprintf(stderr, "!!! Ignoring Error: Wayland: The platform does not support setting the window icon\n");
}
void _glfwGetWindowPosWayland(_GLFWwindow* window, int* xpos, int* ypos)
@@ -2361,8 +2360,7 @@ void _glfwRequestWindowAttentionWayland(_GLFWwindow* window)
void _glfwFocusWindowWayland(_GLFWwindow* window)
{
- _glfwInputError(GLFW_FEATURE_UNAVAILABLE,
- "Wayland: The platform does not support setting the input focus");
+ fprintf(stderr, "!!! Ignoring Error: Wayland: The platform does not support setting the input focus\n");
}
void _glfwSetWindowMonitorWayland(_GLFWwindow* window,

View File

@ -0,0 +1,17 @@
diff --git a/src/init.c b/src/init.c
index 06dbb3f2..a7c6da86 100644
--- a/src/init.c
+++ b/src/init.c
@@ -449,6 +449,12 @@ GLFWAPI int glfwInit(void)
_glfw.initialized = GLFW_TRUE;
glfwDefaultWindowHints();
+
+ fprintf(stderr, "!!! Patched GLFW from https://github.com/Admicos/minecraft-wayland\n"
+ "!!! If any issues with the window, or some issues with rendering, occur, "
+ "first try with the built-in GLFW, and if that solves the issue, report there first.\n"
+ "!!! Use outside Minecraft is untested, and things might break.\n");
+
return GLFW_TRUE;
}

View File

@ -0,0 +1,20 @@
diff --git a/src/platform.c b/src/platform.c
index c5966ae7..3e7442f9 100644
--- a/src/platform.c
+++ b/src/platform.c
@@ -49,12 +49,12 @@ static const struct
#if defined(_GLFW_COCOA)
{ GLFW_PLATFORM_COCOA, _glfwConnectCocoa },
#endif
-#if defined(_GLFW_X11)
- { GLFW_PLATFORM_X11, _glfwConnectX11 },
-#endif
#if defined(_GLFW_WAYLAND)
{ GLFW_PLATFORM_WAYLAND, _glfwConnectWayland },
#endif
+#if defined(_GLFW_X11)
+ { GLFW_PLATFORM_X11, _glfwConnectX11 },
+#endif
};
GLFWbool _glfwSelectPlatform(int desiredID, _GLFWplatform* platform)

View File

@ -0,0 +1,40 @@
diff --git a/src/libdecor.c b/src/libdecor.c
index a9c1106..1aa38b3 100644
--- a/src/libdecor.c
+++ b/src/libdecor.c
@@ -1391,22 +1391,32 @@ calculate_priority(const struct libdecor_plugin_description *plugin_description)
static bool
check_symbol_conflicts(const struct libdecor_plugin_description *plugin_description)
{
+ bool ret = true;
char * const *symbol;
+ void* main_prog = dlopen(NULL, RTLD_LAZY);
+ if (!main_prog) {
+ fprintf(stderr, "Plugin \"%s\" couldn't check conflicting symbols: \"%s\".\n",
+ plugin_description->description, dlerror());
+ return false;
+ }
+
symbol = plugin_description->conflicting_symbols;
while (*symbol) {
dlerror();
- dlsym (RTLD_DEFAULT, *symbol);
+ dlsym (main_prog, *symbol);
if (!dlerror()) {
fprintf(stderr, "Plugin \"%s\" uses conflicting symbol \"%s\".\n",
plugin_description->description, *symbol);
- return false;
+ ret = false;
+ break;
}
symbol++;
}
- return true;
+ dlclose(main_prog);
+ return ret;
}
static struct plugin_loader *

View File

@ -5,7 +5,7 @@ for i in {0..9}; do
test -S "$XDG_RUNTIME_DIR"/discord-ipc-"$i" || ln -sf {app/com.discordapp.Discord,"$XDG_RUNTIME_DIR"}/discord-ipc-"$i"; test -S "$XDG_RUNTIME_DIR"/discord-ipc-"$i" || ln -sf {app/com.discordapp.Discord,"$XDG_RUNTIME_DIR"}/discord-ipc-"$i";
done done
export PATH="${PATH}${PATH:+:}/app/utils/gamescope/bin:/usr/lib/extensions/vulkan/MangoHud/bin" export PATH="${PATH}${PATH:+:}/usr/lib/extensions/vulkan/gamescope/bin:/usr/lib/extensions/vulkan/MangoHud/bin"
export LD_LIBRARY_PATH="${LD_LIBRARY_PATH}${LD_LIBRARY_PATH:+:}/usr/lib/extensions/vulkan/MangoHud/\$LIB/" export VK_LAYER_PATH="/usr/lib/extensions/vulkan/share/vulkan/implicit_layer.d/"
exec /app/bin/prismrun "$@" exec /app/bin/prismrun "$@"

@ -0,0 +1 @@
Subproject commit 45094ca570be383d06df729b6972830ec63bd3df

6
garnix.yaml Normal file
View File

@ -0,0 +1,6 @@
builds:
exclude: []
include:
- "checks.x86_64-linux.*"
- "devShells.*.*"
- "packages.*.*"

View File

@ -6,9 +6,10 @@
* Prism Launcher - Minecraft Launcher * Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 Lenny McLennington <lenny@sneed.church> * Copyright (C) 2022 Lenny McLennington <lenny@sneed.church>
* Copyright (C) 2022 Tayou <tayou@gmx.net> * Copyright (C) 2022 Tayou <git@tayou.org>
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me> * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
* Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com>
* Copyright (C) 2023 seth <getchoo at tuta dot io>
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -442,7 +443,11 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
} }
// seach root path // seach root path
if(!foundLoggingRules) { if(!foundLoggingRules) {
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
logRulesPath = FS::PathCombine(m_rootPath, "share", BuildConfig.LAUNCHER_NAME, logRulesFile);
#else
logRulesPath = FS::PathCombine(m_rootPath, logRulesFile); logRulesPath = FS::PathCombine(m_rootPath, logRulesFile);
#endif
qDebug() << "Testing" << logRulesPath << "..."; qDebug() << "Testing" << logRulesPath << "...";
foundLoggingRules = QFile::exists(logRulesPath); foundLoggingRules = QFile::exists(logRulesPath);
} }
@ -480,6 +485,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
qDebug() << BuildConfig.LAUNCHER_DISPLAYNAME << ", (c) 2013-2021 " << BuildConfig.LAUNCHER_COPYRIGHT; qDebug() << BuildConfig.LAUNCHER_DISPLAYNAME << ", (c) 2013-2021 " << BuildConfig.LAUNCHER_COPYRIGHT;
qDebug() << "Version : " << BuildConfig.printableVersionString(); qDebug() << "Version : " << BuildConfig.printableVersionString();
qDebug() << "Platform : " << BuildConfig.BUILD_PLATFORM;
qDebug() << "Git commit : " << BuildConfig.GIT_COMMIT; qDebug() << "Git commit : " << BuildConfig.GIT_COMMIT;
qDebug() << "Git refspec : " << BuildConfig.GIT_REFSPEC; qDebug() << "Git refspec : " << BuildConfig.GIT_REFSPEC;
if (adjustedBy.size()) if (adjustedBy.size())
@ -577,6 +583,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
// Language // Language
m_settings->registerSetting("Language", QString()); m_settings->registerSetting("Language", QString());
m_settings->registerSetting("UseSystemLocale", false);
// Console // Console
m_settings->registerSetting("ShowConsole", false); m_settings->registerSetting("ShowConsole", false);
@ -613,6 +620,9 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
m_settings->registerSetting("IgnoreJavaCompatibility", false); m_settings->registerSetting("IgnoreJavaCompatibility", false);
m_settings->registerSetting("IgnoreJavaWizard", false); m_settings->registerSetting("IgnoreJavaWizard", false);
// Mod loader settings
m_settings->registerSetting("DisableQuiltBeacon", false);
// Native library workarounds // Native library workarounds
m_settings->registerSetting("UseNativeOpenAL", false); m_settings->registerSetting("UseNativeOpenAL", false);
m_settings->registerSetting("UseNativeGLFW", false); m_settings->registerSetting("UseNativeGLFW", false);
@ -696,9 +706,17 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
m_settings->reset("PastebinCustomAPIBase"); m_settings->reset("PastebinCustomAPIBase");
} }
} }
// meta URL {
// Meta URL
m_settings->registerSetting("MetaURLOverride", ""); m_settings->registerSetting("MetaURLOverride", "");
QUrl metaUrl(m_settings->get("MetaURLOverride").toString());
// get rid of invalid meta urls
if (!metaUrl.isValid() || metaUrl.scheme() != "http" || metaUrl.scheme() != "https")
m_settings->reset("MetaURLOverride");
}
m_settings->registerSetting("CloseAfterLaunch", false); m_settings->registerSetting("CloseAfterLaunch", false);
m_settings->registerSetting("QuitAfterGameStop", false); m_settings->registerSetting("QuitAfterGameStop", false);
@ -919,12 +937,7 @@ bool Application::createSetupWizard()
} }
return false; return false;
}(); }();
bool languageRequired = [&]() bool languageRequired = settings()->get("Language").toString().isEmpty();
{
if (settings()->get("Language").toString().isEmpty())
return true;
return false;
}();
bool pasteInterventionRequired = settings()->get("PastebinURL") != ""; bool pasteInterventionRequired = settings()->get("PastebinURL") != "";
bool themeInterventionRequired = settings()->get("ApplicationTheme") == ""; bool themeInterventionRequired = settings()->get("ApplicationTheme") == "";
bool wizardRequired = javaRequired || languageRequired || pasteInterventionRequired || themeInterventionRequired; bool wizardRequired = javaRequired || languageRequired || pasteInterventionRequired || themeInterventionRequired;
@ -1182,6 +1195,16 @@ QIcon Application::getThemedIcon(const QString& name)
return QIcon::fromTheme(name); return QIcon::fromTheme(name);
} }
QList<CatPack*> Application::getValidCatPacks()
{
return m_themeManager->getValidCatPacks();
}
QString Application::getCatPack(QString catName)
{
return m_themeManager->getCatPack(catName);
}
bool Application::openJsonEditor(const QString& filename) bool Application::openJsonEditor(const QString& filename)
{ {
const QString file = QDir::current().absoluteFilePath(filename); const QString file = QDir::current().absoluteFilePath(filename);
@ -1568,7 +1591,7 @@ QString Application::getJarPath(QString jarFile)
{ {
QStringList potentialPaths = { QStringList potentialPaths = {
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
FS::PathCombine(m_rootPath, "share/" + BuildConfig.LAUNCHER_APP_BINARY_NAME), FS::PathCombine(m_rootPath, "share", BuildConfig.LAUNCHER_NAME),
#endif #endif
FS::PathCombine(m_rootPath, "jars"), FS::PathCombine(m_rootPath, "jars"),
FS::PathCombine(applicationDirPath(), "jars"), FS::PathCombine(applicationDirPath(), "jars"),

View File

@ -2,7 +2,7 @@
/* /*
* Prism Launcher - Minecraft Launcher * Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 Tayou <tayou@gmx.net> * Copyright (C) 2022 Tayou <git@tayou.org>
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me> * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
@ -48,6 +48,7 @@
#include <BaseInstance.h> #include <BaseInstance.h>
#include "minecraft/launch/MinecraftServerTarget.h" #include "minecraft/launch/MinecraftServerTarget.h"
#include "ui/themes/CatPack.h"
class LaunchController; class LaunchController;
class LocalPeer; class LocalPeer;
@ -126,9 +127,11 @@ public:
void setApplicationTheme(const QString& name); void setApplicationTheme(const QString& name);
shared_qobject_ptr<ExternalUpdater> updater() { QList<CatPack*> getValidCatPacks();
return m_updater;
} QString getCatPack(QString catName = "");
shared_qobject_ptr<ExternalUpdater> updater() { return m_updater; }
void triggerUpdateCheck(); void triggerUpdateCheck();

View File

@ -15,15 +15,14 @@
#pragma once #pragma once
#include <memory>
#include <QString>
#include <QMetaType> #include <QMetaType>
#include <QString>
#include <memory>
/*! /*!
* An abstract base class for versions. * An abstract base class for versions.
*/ */
class BaseVersion class BaseVersion {
{
public: public:
using Ptr = std::shared_ptr<BaseVersion>; using Ptr = std::shared_ptr<BaseVersion>;
virtual ~BaseVersion() {} virtual ~BaseVersion() {}
@ -45,14 +44,8 @@ public:
*/ */
virtual QString typeString() const = 0; virtual QString typeString() const = 0;
virtual bool operator<(BaseVersion &a) virtual bool operator<(BaseVersion& a) { return name() < a.name(); };
{ virtual bool operator>(BaseVersion& a) { return name() > a.name(); };
return name() < a.name();
};
virtual bool operator>(BaseVersion &a)
{
return name() > a.name();
};
}; };
Q_DECLARE_METATYPE(BaseVersion::Ptr) Q_DECLARE_METATYPE(BaseVersion::Ptr)

View File

@ -487,6 +487,9 @@ set(API_SOURCES
modplatform/helpers/HashUtils.cpp modplatform/helpers/HashUtils.cpp
modplatform/helpers/OverrideUtils.h modplatform/helpers/OverrideUtils.h
modplatform/helpers/OverrideUtils.cpp modplatform/helpers/OverrideUtils.cpp
modplatform/helpers/ExportToModList.h
modplatform/helpers/ExportToModList.cpp
) )
set(FTB_SOURCES set(FTB_SOURCES
@ -514,6 +517,8 @@ set(FLAME_SOURCES
modplatform/flame/FlameCheckUpdate.h modplatform/flame/FlameCheckUpdate.h
modplatform/flame/FlameInstanceCreationTask.h modplatform/flame/FlameInstanceCreationTask.h
modplatform/flame/FlameInstanceCreationTask.cpp modplatform/flame/FlameInstanceCreationTask.cpp
modplatform/flame/FlamePackExportTask.h
modplatform/flame/FlamePackExportTask.cpp
) )
set(MODRINTH_SOURCES set(MODRINTH_SOURCES
@ -756,6 +761,8 @@ SET(LAUNCHER_SOURCES
ui/themes/SystemTheme.h ui/themes/SystemTheme.h
ui/themes/ThemeManager.cpp ui/themes/ThemeManager.cpp
ui/themes/ThemeManager.h ui/themes/ThemeManager.h
ui/themes/CatPack.cpp
ui/themes/CatPack.h
# Processes # Processes
LaunchController.h LaunchController.h
@ -907,8 +914,10 @@ SET(LAUNCHER_SOURCES
ui/dialogs/EditAccountDialog.h ui/dialogs/EditAccountDialog.h
ui/dialogs/ExportInstanceDialog.cpp ui/dialogs/ExportInstanceDialog.cpp
ui/dialogs/ExportInstanceDialog.h ui/dialogs/ExportInstanceDialog.h
ui/dialogs/ExportMrPackDialog.cpp ui/dialogs/ExportPackDialog.cpp
ui/dialogs/ExportMrPackDialog.h ui/dialogs/ExportPackDialog.h
ui/dialogs/ExportToModListDialog.cpp
ui/dialogs/ExportToModListDialog.h
ui/dialogs/IconPickerDialog.cpp ui/dialogs/IconPickerDialog.cpp
ui/dialogs/IconPickerDialog.h ui/dialogs/IconPickerDialog.h
ui/dialogs/ImportResourceDialog.cpp ui/dialogs/ImportResourceDialog.cpp
@ -1055,7 +1064,8 @@ qt_wrap_ui(LAUNCHER_UI
ui/dialogs/ProfileSelectDialog.ui ui/dialogs/ProfileSelectDialog.ui
ui/dialogs/SkinUploadDialog.ui ui/dialogs/SkinUploadDialog.ui
ui/dialogs/ExportInstanceDialog.ui ui/dialogs/ExportInstanceDialog.ui
ui/dialogs/ExportMrPackDialog.ui ui/dialogs/ExportPackDialog.ui
ui/dialogs/ExportToModListDialog.ui
ui/dialogs/IconPickerDialog.ui ui/dialogs/IconPickerDialog.ui
ui/dialogs/ImportResourceDialog.ui ui/dialogs/ImportResourceDialog.ui
ui/dialogs/MSALoginDialog.ui ui/dialogs/MSALoginDialog.ui

View File

@ -40,6 +40,7 @@
#include <QFileSystemModel> #include <QFileSystemModel>
#include <QSortFilterProxyModel> #include <QSortFilterProxyModel>
#include <QStack> #include <QStack>
#include <algorithm>
#include "FileSystem.h" #include "FileSystem.h"
#include "SeparatorPrefixTree.h" #include "SeparatorPrefixTree.h"
#include "StringUtils.h" #include "StringUtils.h"
@ -254,3 +255,25 @@ bool FileIgnoreProxy::filterAcceptsColumn(int source_column, const QModelIndex&
return true; return true;
} }
bool FileIgnoreProxy::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const
{
QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
QFileSystemModel* fsm = qobject_cast<QFileSystemModel*>(sourceModel());
auto fileInfo = fsm->fileInfo(index);
return !ignoreFile(fileInfo);
}
bool FileIgnoreProxy::ignoreFile(QFileInfo fileInfo) const
{
auto fileName = fileInfo.fileName();
auto path = relPath(fileInfo.absoluteFilePath());
return std::any_of(m_ignoreFiles.cbegin(), m_ignoreFiles.cend(), [fileName](auto iFileName) { return fileName == iFileName; }) ||
m_ignoreFilePaths.covers(path);
}
bool FileIgnoreProxy::filterFile(const QString& fileName) const
{
return blocked.covers(fileName) || ignoreFile(QFileInfo(QDir(root), fileName));
}

View File

@ -36,6 +36,7 @@
#pragma once #pragma once
#include <QFileInfo>
#include <QSortFilterProxyModel> #include <QSortFilterProxyModel>
#include "SeparatorPrefixTree.h" #include "SeparatorPrefixTree.h"
@ -63,10 +64,22 @@ class FileIgnoreProxy : public QSortFilterProxyModel {
inline const SeparatorPrefixTree<'/'>& blockedPaths() const { return blocked; } inline const SeparatorPrefixTree<'/'>& blockedPaths() const { return blocked; }
inline SeparatorPrefixTree<'/'>& blockedPaths() { return blocked; } inline SeparatorPrefixTree<'/'>& blockedPaths() { return blocked; }
// list of file names that need to be removed completely from model
inline QStringList& ignoreFilesWithName() { return m_ignoreFiles; }
// list of relative paths that need to be removed completely from model
inline SeparatorPrefixTree<'/'>& ignoreFilesWithPath() { return m_ignoreFilePaths; }
bool filterFile(const QString& fileName) const;
protected: protected:
bool filterAcceptsColumn(int source_column, const QModelIndex& source_parent) const; bool filterAcceptsColumn(int source_column, const QModelIndex& source_parent) const;
bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const;
bool ignoreFile(QFileInfo file) const;
private: private:
const QString root; const QString root;
SeparatorPrefixTree<'/'> blocked; SeparatorPrefixTree<'/'> blocked;
QStringList m_ignoreFiles;
SeparatorPrefixTree<'/'> m_ignoreFilePaths;
}; };

View File

@ -36,6 +36,7 @@
*/ */
#include "FileSystem.h" #include "FileSystem.h"
#include <QPair>
#include "BuildConfig.h" #include "BuildConfig.h"
@ -102,7 +103,7 @@ namespace fs = ghc::filesystem;
#include <linux/fs.h> #include <linux/fs.h>
#include <sys/ioctl.h> #include <sys/ioctl.h>
#include <unistd.h> #include <unistd.h>
#elif defined(Q_OS_MACOS) || defined(Q_OS_OPENBSD) #elif defined(Q_OS_MACOS)
#include <sys/attr.h> #include <sys/attr.h>
#include <sys/clonefile.h> #include <sys/clonefile.h>
#elif defined(Q_OS_WIN) #elif defined(Q_OS_WIN)
@ -246,6 +247,7 @@ bool copy::operator()(const QString& offset, bool dryRun)
{ {
using copy_opts = fs::copy_options; using copy_opts = fs::copy_options;
m_copied = 0; // reset counter m_copied = 0; // reset counter
m_failedPaths.clear();
// NOTE always deep copy on windows. the alternatives are too messy. // NOTE always deep copy on windows. the alternatives are too messy.
#if defined Q_OS_WIN32 #if defined Q_OS_WIN32
@ -277,6 +279,9 @@ bool copy::operator()(const QString& offset, bool dryRun)
qWarning() << "Failed to copy files:" << QString::fromStdString(err.message()); qWarning() << "Failed to copy files:" << QString::fromStdString(err.message());
qDebug() << "Source file:" << src_path; qDebug() << "Source file:" << src_path;
qDebug() << "Destination file:" << dst_path; qDebug() << "Destination file:" << dst_path;
m_failedPaths.append(dst_path);
emit copyFailed(relative_dst_path);
return;
} }
m_copied++; m_copied++;
emit fileCopied(relative_dst_path); emit fileCopied(relative_dst_path);
@ -773,9 +778,43 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri
destination = PathCombine(getDesktopDir(), RemoveInvalidFilenameChars(name)); destination = PathCombine(getDesktopDir(), RemoveInvalidFilenameChars(name));
} }
#if defined(Q_OS_MACOS) #if defined(Q_OS_MACOS)
destination += ".command"; // Create the Application
QDir applicationDirectory = QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation) + "/" + BuildConfig.LAUNCHER_NAME + " Instances/";
QFile f(destination); if (!applicationDirectory.mkpath(".")) {
qWarning() << "Couldn't create application directory";
return false;
}
QDir application = applicationDirectory.path() + "/" + name + ".app/";
if (application.exists()) {
qWarning() << "Application already exists!";
return false;
}
if (!application.mkpath(".")) {
qWarning() << "Couldn't create application";
return false;
}
QDir content = application.path() + "/Contents/";
QDir resources = content.path() + "/Resources/";
QDir binaryDir = content.path() + "/MacOS/";
QFile info = content.path() + "/Info.plist";
if (!(content.mkpath(".") && resources.mkpath(".") && binaryDir.mkpath("."))) {
qWarning() << "Couldn't create directories within application";
return false;
}
info.open(QIODevice::WriteOnly | QIODevice::Text);
QFile(icon).rename(resources.path() + "/Icon.icns");
// Create the Command file
QString exec = binaryDir.path() + "/Run.command";
QFile f(exec);
f.open(QIODevice::WriteOnly | QIODevice::Text); f.open(QIODevice::WriteOnly | QIODevice::Text);
QTextStream stream(&f); QTextStream stream(&f);
@ -792,6 +831,28 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri
f.setPermissions(f.permissions() | QFileDevice::ExeOwner | QFileDevice::ExeGroup | QFileDevice::ExeOther); f.setPermissions(f.permissions() | QFileDevice::ExeOwner | QFileDevice::ExeGroup | QFileDevice::ExeOther);
// Generate the Info.plist
QTextStream infoStream(&info);
infoStream << "<?xml version=\"1.0\" encoding=\"UTF-8\"?> \n"
"<!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\" "
"\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">"
"<plist version=\"1.0\">\n"
"<dict>\n"
" <key>CFBundleExecutable</key>\n"
" <string>Run.command</string>\n" // The path to the executable
" <key>CFBundleIconFile</key>\n"
" <string>Icon.icns</string>\n"
" <key>CFBundleName</key>\n"
" <string>" << name << "</string>\n" // Name of the application
" <key>CFBundlePackageType</key>\n"
" <string>APPL</string>\n"
" <key>CFBundleShortVersionString</key>\n"
" <string>1.0</string>\n"
" <key>CFBundleVersion</key>\n"
" <string>1.0</string>\n"
"</dict>\n"
"</plist>";
return true; return true;
#elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) #elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
if (!destination.endsWith(".desktop")) // in case of isFlatpak destination is already populated if (!destination.endsWith(".desktop")) // in case of isFlatpak destination is already populated
@ -1077,6 +1138,7 @@ bool clone::operator()(const QString& offset, bool dryRun)
} }
m_cloned = 0; // reset counter m_cloned = 0; // reset counter
m_failedClones.clear();
auto src = PathCombine(m_src.absolutePath(), offset); auto src = PathCombine(m_src.absolutePath(), offset);
auto dst = PathCombine(m_dst.absolutePath(), offset); auto dst = PathCombine(m_dst.absolutePath(), offset);
@ -1097,6 +1159,9 @@ bool clone::operator()(const QString& offset, bool dryRun)
qDebug() << "Failed to clone files: error" << err.value() << "message" << QString::fromStdString(err.message()); qDebug() << "Failed to clone files: error" << err.value() << "message" << QString::fromStdString(err.message());
qDebug() << "Source file:" << src_path; qDebug() << "Source file:" << src_path;
qDebug() << "Destination file:" << dst_path; qDebug() << "Destination file:" << dst_path;
m_failedClones.append(qMakePair(src_path, dst_path));
emit cloneFailed(src_path, dst_path);
return;
} }
m_cloned++; m_cloned++;
emit fileCloned(src_path, dst_path); emit fileCloned(src_path, dst_path);
@ -1156,7 +1221,7 @@ bool clone_file(const QString& src, const QString& dst, std::error_code& ec)
return false; return false;
} }
#elif defined(Q_OS_MACOS) || defined(Q_OS_OPENBSD) #elif defined(Q_OS_MACOS)
if (!macos_bsd_clonefile(src_path, dst_path, ec)) { if (!macos_bsd_clonefile(src_path, dst_path, ec)) {
qDebug() << "failed macos_bsd_clonefile:"; qDebug() << "failed macos_bsd_clonefile:";
@ -1385,7 +1450,7 @@ bool linux_ficlone(const std::string& src_path, const std::string& dst_path, std
return true; return true;
} }
#elif defined(Q_OS_MACOS) || defined(Q_OS_OPENBSD) #elif defined(Q_OS_MACOS)
bool macos_bsd_clonefile(const std::string& src_path, const std::string& dst_path, std::error_code& ec) bool macos_bsd_clonefile(const std::string& src_path, const std::string& dst_path, std::error_code& ec)
{ {

View File

@ -43,6 +43,7 @@
#include <system_error> #include <system_error>
#include <QDir> #include <QDir>
#include <QPair>
#include <QFlags> #include <QFlags>
#include <QLocalServer> #include <QLocalServer>
#include <QObject> #include <QObject>
@ -112,9 +113,12 @@ class copy : public QObject {
bool operator()(bool dryRun = false) { return operator()(QString(), dryRun); } bool operator()(bool dryRun = false) { return operator()(QString(), dryRun); }
int totalCopied() { return m_copied; } int totalCopied() { return m_copied; }
int totalFailed() { return m_failedPaths.length(); }
QStringList failed() { return m_failedPaths; }
signals: signals:
void fileCopied(const QString& relativeName); void fileCopied(const QString& relativeName);
void copyFailed(const QString& relativeName);
// TODO: maybe add a "shouldCopy" signal in the future? // TODO: maybe add a "shouldCopy" signal in the future?
private: private:
@ -127,6 +131,7 @@ class copy : public QObject {
QDir m_src; QDir m_src;
QDir m_dst; QDir m_dst;
int m_copied; int m_copied;
QStringList m_failedPaths;
}; };
struct LinkPair { struct LinkPair {
@ -471,6 +476,9 @@ class clone : public QObject {
bool operator()(bool dryRun = false) { return operator()(QString(), dryRun); } bool operator()(bool dryRun = false) { return operator()(QString(), dryRun); }
int totalCloned() { return m_cloned; } int totalCloned() { return m_cloned; }
int totalFailed() { return m_failedClones.length(); }
QList<QPair<QString, QString>> failed() { return m_failedClones; }
signals: signals:
void fileCloned(const QString& src, const QString& dst); void fileCloned(const QString& src, const QString& dst);
@ -485,6 +493,7 @@ class clone : public QObject {
QDir m_src; QDir m_src;
QDir m_dst; QDir m_dst;
int m_cloned; int m_cloned;
QList<QPair<QString, QString>> m_failedClones;
}; };
/** /**

View File

@ -99,7 +99,7 @@ void InstanceImportTask::executeTask()
connect(m_filesNetJob.get(), &NetJob::succeeded, this, &InstanceImportTask::downloadSucceeded); connect(m_filesNetJob.get(), &NetJob::succeeded, this, &InstanceImportTask::downloadSucceeded);
connect(m_filesNetJob.get(), &NetJob::progress, this, &InstanceImportTask::downloadProgressChanged); connect(m_filesNetJob.get(), &NetJob::progress, this, &InstanceImportTask::downloadProgressChanged);
connect(m_filesNetJob.get(), &NetJob::stepProgress, this, &InstanceImportTask::propogateStepProgress); connect(m_filesNetJob.get(), &NetJob::stepProgress, this, &InstanceImportTask::propagateStepProgress);
connect(m_filesNetJob.get(), &NetJob::failed, this, &InstanceImportTask::downloadFailed); connect(m_filesNetJob.get(), &NetJob::failed, this, &InstanceImportTask::downloadFailed);
connect(m_filesNetJob.get(), &NetJob::aborted, this, &InstanceImportTask::downloadAborted); connect(m_filesNetJob.get(), &NetJob::aborted, this, &InstanceImportTask::downloadAborted);
@ -293,7 +293,7 @@ void InstanceImportTask::processFlame()
}); });
connect(inst_creation_task.get(), &Task::failed, this, &InstanceImportTask::emitFailed); connect(inst_creation_task.get(), &Task::failed, this, &InstanceImportTask::emitFailed);
connect(inst_creation_task.get(), &Task::progress, this, &InstanceImportTask::setProgress); connect(inst_creation_task.get(), &Task::progress, this, &InstanceImportTask::setProgress);
connect(inst_creation_task.get(), &Task::stepProgress, this, &InstanceImportTask::propogateStepProgress); connect(inst_creation_task.get(), &Task::stepProgress, this, &InstanceImportTask::propagateStepProgress);
connect(inst_creation_task.get(), &Task::status, this, &InstanceImportTask::setStatus); connect(inst_creation_task.get(), &Task::status, this, &InstanceImportTask::setStatus);
connect(inst_creation_task.get(), &Task::details, this, &InstanceImportTask::setDetails); connect(inst_creation_task.get(), &Task::details, this, &InstanceImportTask::setDetails);
@ -385,7 +385,7 @@ void InstanceImportTask::processModrinth()
}); });
connect(inst_creation_task, &Task::failed, this, &InstanceImportTask::emitFailed); connect(inst_creation_task, &Task::failed, this, &InstanceImportTask::emitFailed);
connect(inst_creation_task, &Task::progress, this, &InstanceImportTask::setProgress); connect(inst_creation_task, &Task::progress, this, &InstanceImportTask::setProgress);
connect(inst_creation_task, &Task::stepProgress, this, &InstanceImportTask::propogateStepProgress); connect(inst_creation_task, &Task::stepProgress, this, &InstanceImportTask::propagateStepProgress);
connect(inst_creation_task, &Task::status, this, &InstanceImportTask::setStatus); connect(inst_creation_task, &Task::status, this, &InstanceImportTask::setStatus);
connect(inst_creation_task, &Task::details, this, &InstanceImportTask::setDetails); connect(inst_creation_task, &Task::details, this, &InstanceImportTask::setDetails);
connect(inst_creation_task, &Task::finished, inst_creation_task, &InstanceCreationTask::deleteLater); connect(inst_creation_task, &Task::finished, inst_creation_task, &InstanceCreationTask::deleteLater);

View File

@ -799,7 +799,7 @@ class InstanceStaging : public Task {
connect(child, &Task::status, this, &InstanceStaging::setStatus); connect(child, &Task::status, this, &InstanceStaging::setStatus);
connect(child, &Task::details, this, &InstanceStaging::setDetails); connect(child, &Task::details, this, &InstanceStaging::setDetails);
connect(child, &Task::progress, this, &InstanceStaging::setProgress); connect(child, &Task::progress, this, &InstanceStaging::setProgress);
connect(child, &Task::stepProgress, this, &InstanceStaging::propogateStepProgress); connect(child, &Task::stepProgress, this, &InstanceStaging::propagateStepProgress);
connect(&m_backoffTimer, &QTimer::timeout, this, &InstanceStaging::childSucceded); connect(&m_backoffTimer, &QTimer::timeout, this, &InstanceStaging::childSucceded);
} }

View File

@ -187,8 +187,8 @@ void LaunchController::login() {
switch(m_accountToUse->accountState()) { switch(m_accountToUse->accountState()) {
case AccountState::Offline: { case AccountState::Offline: {
m_session->wants_online = false; m_session->wants_online = false;
// NOTE: fallthrough is intentional
} }
/* fallthrough */
case AccountState::Online: { case AccountState::Online: {
if(!m_session->wants_online) { if(!m_session->wants_online) {
// we ask the user for a player name // we ask the user for a player name
@ -267,8 +267,8 @@ void LaunchController::login() {
// This means some sort of soft error that we can fix with a refresh ... so let's refresh. // This means some sort of soft error that we can fix with a refresh ... so let's refresh.
case AccountState::Unchecked: { case AccountState::Unchecked: {
m_accountToUse->refresh(); m_accountToUse->refresh();
// NOTE: fallthrough intentional
} }
/* fallthrough */
case AccountState::Working: { case AccountState::Working: {
// refresh is in progress, we need to wait for it to finish to proceed. // refresh is in progress, we need to wait for it to finish to proceed.
ProgressDialog progDialog(m_parentWidget); ProgressDialog progDialog(m_parentWidget);
@ -390,7 +390,10 @@ void LaunchController::launchInstance()
m_launcher->prependStep(makeShared<TextPrint>(m_launcher.get(), "Launched instance in " + online_mode + " mode\n", MessageLevel::Launcher)); m_launcher->prependStep(makeShared<TextPrint>(m_launcher.get(), "Launched instance in " + online_mode + " mode\n", MessageLevel::Launcher));
// Prepend Version // Prepend Version
m_launcher->prependStep(makeShared<TextPrint>(m_launcher.get(), BuildConfig.LAUNCHER_DISPLAYNAME + " version: " + BuildConfig.printableVersionString() + "\n\n", MessageLevel::Launcher)); {
auto versionString = QString("%1 version: %2 (%3)").arg(BuildConfig.LAUNCHER_DISPLAYNAME, BuildConfig.printableVersionString(), BuildConfig.BUILD_PLATFORM);
m_launcher->prependStep(makeShared<TextPrint>(m_launcher.get(), versionString + "\n\n", MessageLevel::Launcher));
}
m_launcher->start(); m_launcher->start();
} }

View File

@ -1,7 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-only
/* /*
* PolyMC - Minecraft Launcher * Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -33,56 +34,50 @@
* limitations under the License. * limitations under the License.
*/ */
#include "MMCZip.h"
#include <quazip/quazip.h> #include <quazip/quazip.h>
#include <quazip/quazipdir.h> #include <quazip/quazipdir.h>
#include <quazip/quazipfile.h> #include <quazip/quazipfile.h>
#include "MMCZip.h"
#include "FileSystem.h" #include "FileSystem.h"
#include <QCoreApplication> #include <QCoreApplication>
#include <QDebug> #include <QDebug>
#include <QtConcurrentRun>
namespace MMCZip {
// ours // ours
bool MMCZip::mergeZipFiles(QuaZip *into, QFileInfo from, QSet<QString> &contained, const FilterFunction filter) bool mergeZipFiles(QuaZip* into, QFileInfo from, QSet<QString>& contained, const FilterFunction filter)
{ {
QuaZip modZip(from.filePath()); QuaZip modZip(from.filePath());
modZip.open(QuaZip::mdUnzip); modZip.open(QuaZip::mdUnzip);
QuaZipFile fileInsideMod(&modZip); QuaZipFile fileInsideMod(&modZip);
QuaZipFile zipOutFile(into); QuaZipFile zipOutFile(into);
for (bool more = modZip.goToFirstFile(); more; more = modZip.goToNextFile()) for (bool more = modZip.goToFirstFile(); more; more = modZip.goToNextFile()) {
{
QString filename = modZip.getCurrentFileName(); QString filename = modZip.getCurrentFileName();
if (filter && !filter(filename)) if (filter && !filter(filename)) {
{ qDebug() << "Skipping file " << filename << " from " << from.fileName() << " - filtered";
qDebug() << "Skipping file " << filename << " from "
<< from.fileName() << " - filtered";
continue; continue;
} }
if (contained.contains(filename)) if (contained.contains(filename)) {
{ qDebug() << "Skipping already contained file " << filename << " from " << from.fileName();
qDebug() << "Skipping already contained file " << filename << " from "
<< from.fileName();
continue; continue;
} }
contained.insert(filename); contained.insert(filename);
if (!fileInsideMod.open(QIODevice::ReadOnly)) if (!fileInsideMod.open(QIODevice::ReadOnly)) {
{
qCritical() << "Failed to open " << filename << " from " << from.fileName(); qCritical() << "Failed to open " << filename << " from " << from.fileName();
return false; return false;
} }
QuaZipNewInfo info_out(fileInsideMod.getActualFileName()); QuaZipNewInfo info_out(fileInsideMod.getActualFileName());
if (!zipOutFile.open(QIODevice::WriteOnly, info_out)) if (!zipOutFile.open(QIODevice::WriteOnly, info_out)) {
{
qCritical() << "Failed to open " << filename << " in the jar"; qCritical() << "Failed to open " << filename << " in the jar";
fileInsideMod.close(); fileInsideMod.close();
return false; return false;
} }
if (!JlCompress::copyData(fileInsideMod, zipOutFile)) if (!JlCompress::copyData(fileInsideMod, zipOutFile)) {
{
zipOutFile.close(); zipOutFile.close();
fileInsideMod.close(); fileInsideMod.close();
qCritical() << "Failed to copy data of " << filename << " into the jar"; qCritical() << "Failed to copy data of " << filename << " into the jar";
@ -94,10 +89,11 @@ bool MMCZip::mergeZipFiles(QuaZip *into, QFileInfo from, QSet<QString> &containe
return true; return true;
} }
bool MMCZip::compressDirFiles(QuaZip *zip, QString dir, QFileInfoList files, bool followSymlinks) bool compressDirFiles(QuaZip* zip, QString dir, QFileInfoList files, bool followSymlinks)
{ {
QDir directory(dir); QDir directory(dir);
if (!directory.exists()) return false; if (!directory.exists())
return false;
for (auto e : files) { for (auto e : files) {
auto filePath = directory.relativeFilePath(e.absoluteFilePath()); auto filePath = directory.relativeFilePath(e.absoluteFilePath());
@ -109,13 +105,14 @@ bool MMCZip::compressDirFiles(QuaZip *zip, QString dir, QFileInfoList files, boo
srcPath = e.canonicalFilePath(); srcPath = e.canonicalFilePath();
} }
} }
if( !JlCompress::compressFile(zip, srcPath, filePath)) return false; if (!JlCompress::compressFile(zip, srcPath, filePath))
return false;
} }
return true; return true;
} }
bool MMCZip::compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files, bool followSymlinks) bool compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files, bool followSymlinks)
{ {
QuaZip zip(fileCompressed); QuaZip zip(fileCompressed);
QDir().mkpath(QFileInfo(fileCompressed).absolutePath()); QDir().mkpath(QFileInfo(fileCompressed).absolutePath());
@ -136,11 +133,10 @@ bool MMCZip::compressDirFiles(QString fileCompressed, QString dir, QFileInfoList
} }
// ours // ours
bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<Mod*>& mods) bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<Mod*>& mods)
{ {
QuaZip zipOut(targetJarPath); QuaZip zipOut(targetJarPath);
if (!zipOut.open(QuaZip::mdCreate)) if (!zipOut.open(QuaZip::mdCreate)) {
{
QFile::remove(targetJarPath); QFile::remove(targetJarPath);
qCritical() << "Failed to open the minecraft.jar for modding"; qCritical() << "Failed to open the minecraft.jar for modding";
return false; return false;
@ -151,37 +147,29 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const
// Modify the jar // Modify the jar
// This needs to be done in reverse-order to ensure we respect the loading order of components // This needs to be done in reverse-order to ensure we respect the loading order of components
for (auto i = mods.crbegin(); i != mods.crend(); i++) for (auto i = mods.crbegin(); i != mods.crend(); i++) {
{
const auto* mod = *i; const auto* mod = *i;
// do not merge disabled mods. // do not merge disabled mods.
if (!mod->enabled()) if (!mod->enabled())
continue; continue;
if (mod->type() == ResourceType::ZIPFILE) if (mod->type() == ResourceType::ZIPFILE) {
{ if (!mergeZipFiles(&zipOut, mod->fileinfo(), addedFiles)) {
if (!mergeZipFiles(&zipOut, mod->fileinfo(), addedFiles))
{
zipOut.close(); zipOut.close();
QFile::remove(targetJarPath); QFile::remove(targetJarPath);
qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar."; qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar.";
return false; return false;
} }
} } else if (mod->type() == ResourceType::SINGLEFILE) {
else if (mod->type() == ResourceType::SINGLEFILE)
{
// FIXME: buggy - does not work with addedFiles // FIXME: buggy - does not work with addedFiles
auto filename = mod->fileinfo(); auto filename = mod->fileinfo();
if (!JlCompress::compressFile(&zipOut, filename.absoluteFilePath(), filename.fileName())) if (!JlCompress::compressFile(&zipOut, filename.absoluteFilePath(), filename.fileName())) {
{
zipOut.close(); zipOut.close();
QFile::remove(targetJarPath); QFile::remove(targetJarPath);
qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar."; qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar.";
return false; return false;
} }
addedFiles.insert(filename.fileName()); addedFiles.insert(filename.fileName());
} } else if (mod->type() == ResourceType::FOLDER) {
else if (mod->type() == ResourceType::FOLDER)
{
// untested, but seems to be unused / not possible to reach // untested, but seems to be unused / not possible to reach
// FIXME: buggy - does not work with addedFiles // FIXME: buggy - does not work with addedFiles
auto filename = mod->fileinfo(); auto filename = mod->fileinfo();
@ -190,25 +178,21 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const
dir.cdUp(); dir.cdUp();
QString parent_dir = dir.absolutePath(); QString parent_dir = dir.absolutePath();
auto files = QFileInfoList(); auto files = QFileInfoList();
MMCZip::collectFileListRecursively(what_to_zip, nullptr, &files, nullptr); collectFileListRecursively(what_to_zip, nullptr, &files, nullptr);
for (auto e : files) { for (auto e : files) {
if (addedFiles.contains(e.filePath())) if (addedFiles.contains(e.filePath()))
files.removeAll(e); files.removeAll(e);
} }
if (!MMCZip::compressDirFiles(&zipOut, parent_dir, files)) if (!compressDirFiles(&zipOut, parent_dir, files)) {
{
zipOut.close(); zipOut.close();
QFile::remove(targetJarPath); QFile::remove(targetJarPath);
qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar."; qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar.";
return false; return false;
} }
qDebug() << "Adding folder " << filename.fileName() << " from " qDebug() << "Adding folder " << filename.fileName() << " from " << filename.absoluteFilePath();
<< filename.absoluteFilePath(); } else {
}
else
{
// Make sure we do not continue launching when something is missing or undefined... // Make sure we do not continue launching when something is missing or undefined...
zipOut.close(); zipOut.close();
QFile::remove(targetJarPath); QFile::remove(targetJarPath);
@ -217,8 +201,7 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const
} }
} }
if (!mergeZipFiles(&zipOut, QFileInfo(sourceJarPath), addedFiles, [](const QString key){return !key.contains("META-INF");})) if (!mergeZipFiles(&zipOut, QFileInfo(sourceJarPath), addedFiles, [](const QString key) { return !key.contains("META-INF"); })) {
{
zipOut.close(); zipOut.close();
QFile::remove(targetJarPath); QFile::remove(targetJarPath);
qCritical() << "Failed to insert minecraft.jar contents."; qCritical() << "Failed to insert minecraft.jar contents.";
@ -227,8 +210,7 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const
// Recompress the jar // Recompress the jar
zipOut.close(); zipOut.close();
if (zipOut.getZipError() != 0) if (zipOut.getZipError() != 0) {
{
QFile::remove(targetJarPath); QFile::remove(targetJarPath);
qCritical() << "Failed to finalize minecraft.jar!"; qCritical() << "Failed to finalize minecraft.jar!";
return false; return false;
@ -237,7 +219,7 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const
} }
// ours // ours
QString MMCZip::findFolderOfFileInZip(QuaZip* zip, const QString& what, const QStringList& ignore_paths, const QString& root) QString findFolderOfFileInZip(QuaZip* zip, const QString& what, const QStringList& ignore_paths, const QString& root)
{ {
QuaZipDir rootDir(zip, root); QuaZipDir rootDir(zip, root);
for (auto&& fileName : rootDir.entryList(QDir::Files)) { for (auto&& fileName : rootDir.entryList(QDir::Files)) {
@ -261,27 +243,23 @@ QString MMCZip::findFolderOfFileInZip(QuaZip* zip, const QString& what, const QS
} }
// ours // ours
bool MMCZip::findFilesInZip(QuaZip * zip, const QString & what, QStringList & result, const QString &root) bool findFilesInZip(QuaZip* zip, const QString& what, QStringList& result, const QString& root)
{ {
QuaZipDir rootDir(zip, root); QuaZipDir rootDir(zip, root);
for(auto fileName: rootDir.entryList(QDir::Files)) for (auto fileName : rootDir.entryList(QDir::Files)) {
{ if (fileName == what) {
if(fileName == what)
{
result.append(root); result.append(root);
return true; return true;
} }
} }
for(auto fileName: rootDir.entryList(QDir::Dirs)) for (auto fileName : rootDir.entryList(QDir::Dirs)) {
{
findFilesInZip(zip, what, result, root + fileName); findFilesInZip(zip, what, result, root + fileName);
} }
return !result.isEmpty(); return !result.isEmpty();
} }
// ours // ours
std::optional<QStringList> MMCZip::extractSubDir(QuaZip *zip, const QString & subdir, const QString &target) std::optional<QStringList> extractSubDir(QuaZip* zip, const QString& subdir, const QString& target)
{ {
auto target_top_dir = QUrl::fromLocalFile(target); auto target_top_dir = QUrl::fromLocalFile(target);
@ -292,13 +270,10 @@ std::optional<QStringList> MMCZip::extractSubDir(QuaZip *zip, const QString & su
if (numEntries < 0) { if (numEntries < 0) {
qWarning() << "Failed to enumerate files in archive"; qWarning() << "Failed to enumerate files in archive";
return std::nullopt; return std::nullopt;
} } else if (numEntries == 0) {
else if(numEntries == 0) {
qDebug() << "Extracting empty archives seems odd..."; qDebug() << "Extracting empty archives seems odd...";
return extracted; return extracted;
} } else if (!zip->goToFirstFile()) {
else if (!zip->goToFirstFile())
{
qWarning() << "Failed to seek to first file in zip"; qWarning() << "Failed to seek to first file in zip";
return std::nullopt; return std::nullopt;
} }
@ -334,7 +309,8 @@ std::optional<QStringList> MMCZip::extractSubDir(QuaZip *zip, const QString & su
} }
if (!target_top_dir.isParentOf(QUrl::fromLocalFile(target_file_path))) { if (!target_top_dir.isParentOf(QUrl::fromLocalFile(target_file_path))) {
qWarning() << "Extracting" << relative_file_name << "was cancelled, because it was effectively outside of the target path" << target; qWarning() << "Extracting" << relative_file_name << "was cancelled, because it was effectively outside of the target path"
<< target;
return std::nullopt; return std::nullopt;
} }
@ -345,7 +321,8 @@ std::optional<QStringList> MMCZip::extractSubDir(QuaZip *zip, const QString & su
} }
extracted.append(target_file_path); extracted.append(target_file_path);
QFile::setPermissions(target_file_path, QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser); QFile::setPermissions(target_file_path,
QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser);
qDebug() << "Extracted file" << relative_file_name << "to" << target_file_path; qDebug() << "Extracted file" << relative_file_name << "to" << target_file_path;
} while (zip->goToNextFile()); } while (zip->goToNextFile());
@ -354,51 +331,50 @@ std::optional<QStringList> MMCZip::extractSubDir(QuaZip *zip, const QString & su
} }
// ours // ours
bool MMCZip::extractRelFile(QuaZip *zip, const QString &file, const QString &target) bool extractRelFile(QuaZip* zip, const QString& file, const QString& target)
{ {
return JlCompress::extractFile(zip, file, target); return JlCompress::extractFile(zip, file, target);
} }
// ours // ours
std::optional<QStringList> MMCZip::extractDir(QString fileCompressed, QString dir) std::optional<QStringList> extractDir(QString fileCompressed, QString dir)
{ {
QuaZip zip(fileCompressed); QuaZip zip(fileCompressed);
if (!zip.open(QuaZip::mdUnzip)) if (!zip.open(QuaZip::mdUnzip)) {
{
// check if this is a minimum size empty zip file... // check if this is a minimum size empty zip file...
QFileInfo fileInfo(fileCompressed); QFileInfo fileInfo(fileCompressed);
if (fileInfo.size() == 22) { if (fileInfo.size() == 22) {
return QStringList(); return QStringList();
} }
qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError();; qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError();
;
return std::nullopt; return std::nullopt;
} }
return MMCZip::extractSubDir(&zip, "", dir); return extractSubDir(&zip, "", dir);
} }
// ours // ours
std::optional<QStringList> MMCZip::extractDir(QString fileCompressed, QString subdir, QString dir) std::optional<QStringList> extractDir(QString fileCompressed, QString subdir, QString dir)
{ {
QuaZip zip(fileCompressed); QuaZip zip(fileCompressed);
if (!zip.open(QuaZip::mdUnzip)) if (!zip.open(QuaZip::mdUnzip)) {
{
// check if this is a minimum size empty zip file... // check if this is a minimum size empty zip file...
QFileInfo fileInfo(fileCompressed); QFileInfo fileInfo(fileCompressed);
if (fileInfo.size() == 22) { if (fileInfo.size() == 22) {
return QStringList(); return QStringList();
} }
qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError();; qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError();
;
return std::nullopt; return std::nullopt;
} }
return MMCZip::extractSubDir(&zip, subdir, dir); return extractSubDir(&zip, subdir, dir);
} }
// ours // ours
bool MMCZip::extractFile(QString fileCompressed, QString file, QString target) bool extractFile(QString fileCompressed, QString file, QString target)
{ {
QuaZip zip(fileCompressed); QuaZip zip(fileCompressed);
if (!zip.open(QuaZip::mdUnzip)) if (!zip.open(QuaZip::mdUnzip)) {
{
// check if this is a minimum size empty zip file... // check if this is a minimum size empty zip file...
QFileInfo fileInfo(fileCompressed); QFileInfo fileInfo(fileCompressed);
if (fileInfo.size() == 22) { if (fileInfo.size() == 22) {
@ -407,13 +383,14 @@ bool MMCZip::extractFile(QString fileCompressed, QString file, QString target)
qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError(); qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError();
return false; return false;
} }
return MMCZip::extractRelFile(&zip, file, target); return extractRelFile(&zip, file, target);
} }
bool MMCZip::collectFileListRecursively(const QString& rootDir, const QString& subDir, QFileInfoList *files, bool collectFileListRecursively(const QString& rootDir, const QString& subDir, QFileInfoList* files, FilterFunction excludeFilter)
MMCZip::FilterFunction excludeFilter) { {
QDir rootDirectory(rootDir); QDir rootDirectory(rootDir);
if (!rootDirectory.exists()) return false; if (!rootDirectory.exists())
return false;
QDir directory; QDir directory;
if (subDir == nullptr) if (subDir == nullptr)
@ -421,7 +398,8 @@ bool MMCZip::collectFileListRecursively(const QString& rootDir, const QString& s
else else
directory = QDir(subDir); directory = QDir(subDir);
if (!directory.exists()) return false; // shouldn't ever happen if (!directory.exists())
return false; // shouldn't ever happen
// recurse directories // recurse directories
QFileInfoList entries = directory.entryInfoList(QDir::AllDirs | QDir::NoDotAndDotDot | QDir::Hidden); QFileInfoList entries = directory.entryInfoList(QDir::AllDirs | QDir::NoDotAndDotDot | QDir::Hidden);
@ -439,7 +417,88 @@ bool MMCZip::collectFileListRecursively(const QString& rootDir, const QString& s
continue; continue;
} }
files->append(e); // we want the original paths for MMCZip::compressDirFiles files->append(e); // we want the original paths for compressDirFiles
} }
return true; return true;
} }
void ExportToZipTask::executeTask()
{
setStatus("Adding files...");
setProgress(0, m_files.length());
m_build_zip_future = QtConcurrent::run(QThreadPool::globalInstance(), [this]() { return exportZip(); });
connect(&m_build_zip_watcher, &QFutureWatcher<ZipResult>::finished, this, &ExportToZipTask::finish);
m_build_zip_watcher.setFuture(m_build_zip_future);
}
auto ExportToZipTask::exportZip() -> ZipResult
{
if (!m_dir.exists()) {
return ZipResult(tr("Folder doesn't exist"));
}
if (!m_output.isOpen() && !m_output.open(QuaZip::mdCreate)) {
return ZipResult(tr("Could not create file"));
}
for (auto fileName : m_extra_files.keys()) {
if (m_build_zip_future.isCanceled())
return ZipResult();
QuaZipFile indexFile(&m_output);
if (!indexFile.open(QIODevice::WriteOnly, QuaZipNewInfo(fileName))) {
return ZipResult(tr("Could not create:") + fileName);
}
indexFile.write(m_extra_files[fileName]);
}
for (const QFileInfo& file : m_files) {
if (m_build_zip_future.isCanceled())
return ZipResult();
auto absolute = file.absoluteFilePath();
auto relative = m_dir.relativeFilePath(absolute);
setStatus("Compresing: " + relative);
setProgress(m_progress + 1, m_progressTotal);
if (m_follow_symlinks) {
if (file.isSymLink())
absolute = file.symLinkTarget();
else
absolute = file.canonicalFilePath();
}
if (!m_exclude_files.contains(relative) && !JlCompress::compressFile(&m_output, absolute, m_destination_prefix + relative)) {
return ZipResult(tr("Could not read and compress %1").arg(relative));
}
}
m_output.close();
if (m_output.getZipError() != 0) {
return ZipResult(tr("A zip error occurred"));
}
return ZipResult();
}
void ExportToZipTask::finish()
{
if (m_build_zip_future.isCanceled()) {
QFile::remove(m_output_path);
emitAborted();
} else if (auto result = m_build_zip_future.result(); result.has_value()) {
QFile::remove(m_output_path);
emitFailed(result.value());
} else {
emitSucceeded();
}
}
bool ExportToZipTask::abort()
{
if (m_build_zip_future.isRunning()) {
m_build_zip_future.cancel();
// NOTE: Here we don't do `emitAborted()` because it will be done when `m_build_zip_future` actually cancels, which may not occur
// immediately.
return true;
}
return false;
}
} // namespace MMCZip

View File

@ -1,7 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-only
/* /*
* PolyMC - Minecraft Launcher * Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -35,24 +36,28 @@
#pragma once #pragma once
#include <QString> #include <quazip.h>
#include <QFileInfo>
#include <QSet>
#include "minecraft/mod/Mod.h"
#include <functional>
#include <quazip/JlCompress.h> #include <quazip/JlCompress.h>
#include <QDir>
#include <QFileInfo>
#include <QFuture>
#include <QFutureWatcher>
#include <QHash>
#include <QSet>
#include <QString>
#include <functional>
#include <memory>
#include <optional> #include <optional>
#include "minecraft/mod/Mod.h"
#include "tasks/Task.h"
namespace MMCZip namespace MMCZip {
{
using FilterFunction = std::function<bool(const QString&)>; using FilterFunction = std::function<bool(const QString&)>;
/** /**
* Merge two zip files, using a filter function * Merge two zip files, using a filter function
*/ */
bool mergeZipFiles(QuaZip *into, QFileInfo from, QSet<QString> &contained, bool mergeZipFiles(QuaZip* into, QFileInfo from, QSet<QString>& contained, const FilterFunction filter = nullptr);
const FilterFunction filter = nullptr);
/** /**
* Compress directory, by providing a list of files to compress * Compress directory, by providing a list of files to compress
@ -141,4 +146,47 @@ namespace MMCZip
* \return true for success or false for failure * \return true for success or false for failure
*/ */
bool collectFileListRecursively(const QString& rootDir, const QString& subDir, QFileInfoList* files, FilterFunction excludeFilter); bool collectFileListRecursively(const QString& rootDir, const QString& subDir, QFileInfoList* files, FilterFunction excludeFilter);
}
class ExportToZipTask : public Task {
public:
ExportToZipTask(QString outputPath, QDir dir, QFileInfoList files, QString destinationPrefix = "", bool followSymlinks = false)
: m_output_path(outputPath)
, m_output(outputPath)
, m_dir(dir)
, m_files(files)
, m_destination_prefix(destinationPrefix)
, m_follow_symlinks(followSymlinks)
{
setAbortable(true);
};
ExportToZipTask(QString outputPath, QString dir, QFileInfoList files, QString destinationPrefix = "", bool followSymlinks = false)
: ExportToZipTask(outputPath, QDir(dir), files, destinationPrefix, followSymlinks){};
virtual ~ExportToZipTask() = default;
void setExcludeFiles(QStringList excludeFiles) { m_exclude_files = excludeFiles; }
void addExtraFile(QString fileName, QByteArray data) { m_extra_files.insert(fileName, data); }
typedef std::optional<QString> ZipResult;
protected:
virtual void executeTask() override;
bool abort() override;
ZipResult exportZip();
void finish();
private:
QString m_output_path;
QuaZip m_output;
QDir m_dir;
QFileInfoList m_files;
QString m_destination_prefix;
bool m_follow_symlinks;
QStringList m_exclude_files;
QHash<QString, QByteArray> m_extra_files;
QFuture<ZipResult> m_build_zip_future;
QFutureWatcher<ZipResult> m_build_zip_watcher;
};
} // namespace MMCZip

View File

@ -54,7 +54,7 @@ ResourceDownloadTask::ResourceDownloadTask(ModPlatform::IndexedPack::Ptr pack,
m_filesNetJob->addNetAction(Net::Download::makeFile(m_pack_version.downloadUrl, dir.absoluteFilePath(getFilename()))); m_filesNetJob->addNetAction(Net::Download::makeFile(m_pack_version.downloadUrl, dir.absoluteFilePath(getFilename())));
connect(m_filesNetJob.get(), &NetJob::succeeded, this, &ResourceDownloadTask::downloadSucceeded); connect(m_filesNetJob.get(), &NetJob::succeeded, this, &ResourceDownloadTask::downloadSucceeded);
connect(m_filesNetJob.get(), &NetJob::progress, this, &ResourceDownloadTask::downloadProgressChanged); connect(m_filesNetJob.get(), &NetJob::progress, this, &ResourceDownloadTask::downloadProgressChanged);
connect(m_filesNetJob.get(), &NetJob::stepProgress, this, &ResourceDownloadTask::propogateStepProgress); connect(m_filesNetJob.get(), &NetJob::stepProgress, this, &ResourceDownloadTask::propagateStepProgress);
connect(m_filesNetJob.get(), &NetJob::failed, this, &ResourceDownloadTask::downloadFailed); connect(m_filesNetJob.get(), &NetJob::failed, this, &ResourceDownloadTask::downloadFailed);
addTask(m_filesNetJob); addTask(m_filesNetJob);

View File

@ -193,33 +193,23 @@ QVariant VersionProxyModel::data(const QModelIndex &index, int role) const
} }
case Qt::ToolTipRole: case Qt::ToolTipRole:
{ {
switch(column) if(column == Name && hasRecommended)
{
case Name:
{
if(hasRecommended)
{ {
auto value = sourceModel()->data(parentIndex, BaseVersionList::RecommendedRole); auto value = sourceModel()->data(parentIndex, BaseVersionList::RecommendedRole);
if(value.toBool()) if(value.toBool())
{ {
return tr("Recommended"); return tr("Recommended");
} } else if(hasLatest) {
else if(hasLatest)
{
auto value = sourceModel()->data(parentIndex, BaseVersionList::LatestRole); auto value = sourceModel()->data(parentIndex, BaseVersionList::LatestRole);
if(value.toBool()) if(value.toBool())
{ {
return tr("Latest"); return tr("Latest");
} }
} }
} } else {
}
default:
{
return sourceModel()->data(parentIndex, BaseVersionList::VersionIdRole); return sourceModel()->data(parentIndex, BaseVersionList::VersionIdRole);
} }
} }
}
case Qt::DecorationRole: case Qt::DecorationRole:
{ {
switch(column) switch(column)

View File

@ -1,5 +1,24 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "JavaInstall.h" #include "JavaInstall.h"
#include "BaseVersion.h"
#include "StringUtils.h" #include "StringUtils.h"
bool JavaInstall::operator<(const JavaInstall& rhs) bool JavaInstall::operator<(const JavaInstall& rhs)
@ -7,12 +26,10 @@ bool JavaInstall::operator<(const JavaInstall &rhs)
auto archCompare = StringUtils::naturalCompare(arch, rhs.arch, Qt::CaseInsensitive); auto archCompare = StringUtils::naturalCompare(arch, rhs.arch, Qt::CaseInsensitive);
if (archCompare != 0) if (archCompare != 0)
return archCompare < 0; return archCompare < 0;
if(id < rhs.id) if (id < rhs.id) {
{
return true; return true;
} }
if(id > rhs.id) if (id > rhs.id) {
{
return false; return false;
} }
return StringUtils::naturalCompare(path, rhs.path, Qt::CaseInsensitive) < 0; return StringUtils::naturalCompare(path, rhs.path, Qt::CaseInsensitive) < 0;
@ -27,3 +44,21 @@ bool JavaInstall::operator>(const JavaInstall &rhs)
{ {
return (!operator<(rhs)) && (!operator==(rhs)); return (!operator<(rhs)) && (!operator==(rhs));
} }
bool JavaInstall::operator<(BaseVersion& a)
{
try {
return operator<(dynamic_cast<JavaInstall&>(a));
} catch (const std::bad_cast& e) {
return BaseVersion::operator<(a);
}
}
bool JavaInstall::operator>(BaseVersion& a)
{
try {
return operator>(dynamic_cast<JavaInstall&>(a));
} catch (const std::bad_cast& e) {
return BaseVersion::operator>(a);
}
}

View File

@ -1,30 +1,37 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once #pragma once
#include "BaseVersion.h" #include "BaseVersion.h"
#include "JavaVersion.h" #include "JavaVersion.h"
struct JavaInstall : public BaseVersion struct JavaInstall : public BaseVersion {
{
JavaInstall() {} JavaInstall() {}
JavaInstall(QString id, QString arch, QString path) JavaInstall(QString id, QString arch, QString path) : id(id), arch(arch), path(path) {}
: id(id), arch(arch), path(path) virtual QString descriptor() { return id.toString(); }
{
}
virtual QString descriptor()
{
return id.toString();
}
virtual QString name() virtual QString name() { return id.toString(); }
{
return id.toString();
}
virtual QString typeString() const virtual QString typeString() const { return arch; }
{
return arch;
}
virtual bool operator<(BaseVersion& a) override;
virtual bool operator>(BaseVersion& a) override;
bool operator<(const JavaInstall& rhs); bool operator<(const JavaInstall& rhs);
bool operator==(const JavaInstall& rhs); bool operator==(const JavaInstall& rhs);
bool operator>(const JavaInstall& rhs); bool operator>(const JavaInstall& rhs);

View File

@ -28,7 +28,7 @@ void Update::executeTask()
{ {
connect(m_updateTask.get(), &Task::finished, this, &Update::updateFinished); connect(m_updateTask.get(), &Task::finished, this, &Update::updateFinished);
connect(m_updateTask.get(), &Task::progress, this, &Update::setProgress); connect(m_updateTask.get(), &Task::progress, this, &Update::setProgress);
connect(m_updateTask.get(), &Task::stepProgress, this, &Update::propogateStepProgress); connect(m_updateTask.get(), &Task::stepProgress, this, &Update::propagateStepProgress);
connect(m_updateTask.get(), &Task::status, this, &Update::setStatus); connect(m_updateTask.get(), &Task::status, this, &Update::setStatus);
connect(m_updateTask.get(), &Task::details, this, &Update::setDetails); connect(m_updateTask.get(), &Task::details, this, &Update::setDetails);
emit progressReportingRequest(); emit progressReportingRequest();

View File

@ -45,10 +45,10 @@ QVariant Index::data(const QModelIndex &index, int role) const
switch (role) switch (role)
{ {
case Qt::DisplayRole: case Qt::DisplayRole:
switch (index.column()) if (index.column() == 0) {
{ return list->humanReadable();
case 0: return list->humanReadable(); } else {
default: break; break;
} }
case UidRole: return list->uid(); case UidRole: return list->uid();
case NameRole: return list->name(); case NameRole: return list->name();

View File

@ -4,6 +4,7 @@
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 Jamie Mansfield <jmansfield@cadixdev.org> * Copyright (C) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me> * Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
* Copyright (c) 2023 seth <getchoo at tuta dot io>
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -186,6 +187,10 @@ void MinecraftInstance::loadSpecificSettings()
m_settings->registerOverride(global_settings->getSetting("CloseAfterLaunch"), miscellaneousOverride); m_settings->registerOverride(global_settings->getSetting("CloseAfterLaunch"), miscellaneousOverride);
m_settings->registerOverride(global_settings->getSetting("QuitAfterGameStop"), miscellaneousOverride); m_settings->registerOverride(global_settings->getSetting("QuitAfterGameStop"), miscellaneousOverride);
// Mod loader specific options
auto modLoaderSettings = m_settings->registerSetting("OverrideModLoaderSettings", false);
m_settings->registerOverride(global_settings->getSetting("DisableQuiltBeacon"), modLoaderSettings);
m_settings->set("InstanceType", "OneSix"); m_settings->set("InstanceType", "OneSix");
} }
@ -391,6 +396,12 @@ QStringList MinecraftInstance::extraArguments()
agent->library()->getApplicableFiles(runtimeContext(), jar, temp1, temp2, temp3, getLocalLibraryPath()); agent->library()->getApplicableFiles(runtimeContext(), jar, temp1, temp2, temp3, getLocalLibraryPath());
list.append("-javaagent:"+jar[0]+(agent->argument().isEmpty() ? "" : "="+agent->argument())); list.append("-javaagent:"+jar[0]+(agent->argument().isEmpty() ? "" : "="+agent->argument()));
} }
{
const auto loaders = version->getModLoaders();
if (loaders.has_value() && loaders.value() & ResourceAPI::Quilt && settings()->get("DisableQuiltBeacon").toBool())
list.append("-Dloader.disable_beacon=true");
}
return list; return list;
} }
@ -832,7 +843,7 @@ QMap<QString, QString> MinecraftInstance::createCensorFilterFromSession(AuthSess
{ {
addToFilter(sessionRef.session, tr("<SESSION ID>")); addToFilter(sessionRef.session, tr("<SESSION ID>"));
} }
if (sessionRef.access_token != "offline") { if (sessionRef.access_token != "0") {
addToFilter(sessionRef.access_token, tr("<ACCESS TOKEN>")); addToFilter(sessionRef.access_token, tr("<ACCESS TOKEN>"));
} }
if(sessionRef.client_token.size()) { if(sessionRef.client_token.size()) {
@ -1112,36 +1123,27 @@ JavaVersion MinecraftInstance::getJavaVersion()
std::shared_ptr<ModFolderModel> MinecraftInstance::loaderModList() std::shared_ptr<ModFolderModel> MinecraftInstance::loaderModList()
{ {
if (!m_loader_mod_list) if (!m_loader_mod_list) {
{
bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool();
m_loader_mod_list.reset(new ModFolderModel(modsRoot(), this, is_indexed)); m_loader_mod_list.reset(new ModFolderModel(modsRoot(), this, is_indexed));
m_loader_mod_list->disableInteraction(isRunning());
connect(this, &BaseInstance::runningStatusChanged, m_loader_mod_list.get(), &ModFolderModel::disableInteraction);
} }
return m_loader_mod_list; return m_loader_mod_list;
} }
std::shared_ptr<ModFolderModel> MinecraftInstance::coreModList() std::shared_ptr<ModFolderModel> MinecraftInstance::coreModList()
{ {
if (!m_core_mod_list) if (!m_core_mod_list) {
{
bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool();
m_core_mod_list.reset(new ModFolderModel(coreModsDir(), this, is_indexed)); m_core_mod_list.reset(new ModFolderModel(coreModsDir(), this, is_indexed));
m_core_mod_list->disableInteraction(isRunning());
connect(this, &BaseInstance::runningStatusChanged, m_core_mod_list.get(), &ModFolderModel::disableInteraction);
} }
return m_core_mod_list; return m_core_mod_list;
} }
std::shared_ptr<ModFolderModel> MinecraftInstance::nilModList() std::shared_ptr<ModFolderModel> MinecraftInstance::nilModList()
{ {
if (!m_nil_mod_list) if (!m_nil_mod_list) {
{
bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool();
m_nil_mod_list.reset(new ModFolderModel(nilModsDir(), this, is_indexed, false)); m_nil_mod_list.reset(new ModFolderModel(nilModsDir(), this, is_indexed, false));
m_nil_mod_list->disableInteraction(isRunning());
connect(this, &BaseInstance::runningStatusChanged, m_nil_mod_list.get(), &ModFolderModel::disableInteraction);
} }
return m_nil_mod_list; return m_nil_mod_list;
} }

View File

@ -22,7 +22,7 @@ void MinecraftLoadAndCheck::executeTask()
connect(m_task.get(), &Task::failed, this, &MinecraftLoadAndCheck::subtaskFailed); connect(m_task.get(), &Task::failed, this, &MinecraftLoadAndCheck::subtaskFailed);
connect(m_task.get(), &Task::aborted, this, [this]{ subtaskFailed(tr("Aborted")); }); connect(m_task.get(), &Task::aborted, this, [this]{ subtaskFailed(tr("Aborted")); });
connect(m_task.get(), &Task::progress, this, &MinecraftLoadAndCheck::progress); connect(m_task.get(), &Task::progress, this, &MinecraftLoadAndCheck::progress);
connect(m_task.get(), &Task::stepProgress, this, &MinecraftLoadAndCheck::propogateStepProgress); connect(m_task.get(), &Task::stepProgress, this, &MinecraftLoadAndCheck::propagateStepProgress);
connect(m_task.get(), &Task::status, this, &MinecraftLoadAndCheck::setStatus); connect(m_task.get(), &Task::status, this, &MinecraftLoadAndCheck::setStatus);
} }

View File

@ -100,7 +100,7 @@ void MinecraftUpdate::next()
disconnect(task.get(), &Task::failed, this, &MinecraftUpdate::subtaskFailed); disconnect(task.get(), &Task::failed, this, &MinecraftUpdate::subtaskFailed);
disconnect(task.get(), &Task::aborted, this, &Task::abort); disconnect(task.get(), &Task::aborted, this, &Task::abort);
disconnect(task.get(), &Task::progress, this, &MinecraftUpdate::progress); disconnect(task.get(), &Task::progress, this, &MinecraftUpdate::progress);
disconnect(task.get(), &Task::stepProgress, this, &MinecraftUpdate::propogateStepProgress); disconnect(task.get(), &Task::stepProgress, this, &MinecraftUpdate::propagateStepProgress);
disconnect(task.get(), &Task::status, this, &MinecraftUpdate::setStatus); disconnect(task.get(), &Task::status, this, &MinecraftUpdate::setStatus);
disconnect(task.get(), &Task::details, this, &MinecraftUpdate::setDetails); disconnect(task.get(), &Task::details, this, &MinecraftUpdate::setDetails);
} }
@ -120,7 +120,7 @@ void MinecraftUpdate::next()
connect(task.get(), &Task::failed, this, &MinecraftUpdate::subtaskFailed); connect(task.get(), &Task::failed, this, &MinecraftUpdate::subtaskFailed);
connect(task.get(), &Task::aborted, this, &Task::abort); connect(task.get(), &Task::aborted, this, &Task::abort);
connect(task.get(), &Task::progress, this, &MinecraftUpdate::progress); connect(task.get(), &Task::progress, this, &MinecraftUpdate::progress);
connect(task.get(), &Task::stepProgress, this, &MinecraftUpdate::propogateStepProgress); connect(task.get(), &Task::stepProgress, this, &MinecraftUpdate::propagateStepProgress);
connect(task.get(), &Task::status, this, &MinecraftUpdate::setStatus); connect(task.get(), &Task::status, this, &MinecraftUpdate::setStatus);
connect(task.get(), &Task::details, this, &MinecraftUpdate::setDetails); connect(task.get(), &Task::details, this, &MinecraftUpdate::setDetails);
// if the task is already running, do not start it again // if the task is already running, do not start it again

View File

@ -65,7 +65,8 @@
static const QMap<QString, ResourceAPI::ModLoaderType> modloaderMapping{ static const QMap<QString, ResourceAPI::ModLoaderType> modloaderMapping{
{"net.minecraftforge", ResourceAPI::Forge}, {"net.minecraftforge", ResourceAPI::Forge},
{"net.fabricmc.fabric-loader", ResourceAPI::Fabric}, {"net.fabricmc.fabric-loader", ResourceAPI::Fabric},
{"org.quiltmc.quilt-loader", ResourceAPI::Quilt} {"org.quiltmc.quilt-loader", ResourceAPI::Quilt},
{"com.mumfrey.liteloader", ResourceAPI::LiteLoader}
}; };
PackProfile::PackProfile(MinecraftInstance * instance) PackProfile::PackProfile(MinecraftInstance * instance)

View File

@ -374,6 +374,10 @@ bool AccountData::resumeStateFromV3(QJsonObject data) {
} }
yggdrasilToken = tokenFromJSONV3(data, "ygg"); yggdrasilToken = tokenFromJSONV3(data, "ygg");
// versions before 7.2 used "offline" as the offline token
if (yggdrasilToken.token == "offline")
yggdrasilToken.token = "0";
minecraftProfile = profileFromJSONV3(data, "profile"); minecraftProfile = profileFromJSONV3(data, "profile");
if(!entitlementFromJSONV3(data, minecraftEntitlement)) { if(!entitlementFromJSONV3(data, minecraftEntitlement)) {
if(minecraftProfile.validity != Katabasis::Validity::None) { if(minecraftProfile.validity != Katabasis::Validity::None) {

View File

@ -328,6 +328,9 @@ QVariant AccountList::data(const QModelIndex &index, int role) const
case AccountState::Gone: { case AccountState::Gone: {
return tr("Gone", "Account status"); return tr("Gone", "Account status");
} }
default: {
return tr("Unknown", "Account status");
}
} }
} }
@ -354,12 +357,13 @@ QVariant AccountList::data(const QModelIndex &index, int role) const
return QVariant::fromValue(account); return QVariant::fromValue(account);
case Qt::CheckStateRole: case Qt::CheckStateRole:
switch (index.column()) if (index.column() == ProfileNameColumn) {
{
case ProfileNameColumn:
return account == m_defaultAccount ? Qt::Checked : Qt::Unchecked; return account == m_defaultAccount ? Qt::Checked : Qt::Unchecked;
} else {
return QVariant();
} }
default: default:
return QVariant(); return QVariant();
} }

View File

@ -26,6 +26,7 @@ bool AuthSession::MakeOffline(QString offline_playername)
return false; return false;
} }
session = "-"; session = "-";
access_token = "0";
player_name = offline_playername; player_name = offline_playername;
status = PlayableOffline; status = PlayableOffline;
return true; return true;

View File

@ -37,6 +37,7 @@
#include "MinecraftAccount.h" #include "MinecraftAccount.h"
#include <QCryptographicHash>
#include <QUuid> #include <QUuid>
#include <QJsonObject> #include <QJsonObject>
#include <QJsonArray> #include <QJsonArray>
@ -93,14 +94,14 @@ MinecraftAccountPtr MinecraftAccount::createOffline(const QString &username)
{ {
auto account = makeShared<MinecraftAccount>(); auto account = makeShared<MinecraftAccount>();
account->data.type = AccountType::Offline; account->data.type = AccountType::Offline;
account->data.yggdrasilToken.token = "offline"; account->data.yggdrasilToken.token = "0";
account->data.yggdrasilToken.validity = Katabasis::Validity::Certain; account->data.yggdrasilToken.validity = Katabasis::Validity::Certain;
account->data.yggdrasilToken.issueInstant = QDateTime::currentDateTimeUtc(); account->data.yggdrasilToken.issueInstant = QDateTime::currentDateTimeUtc();
account->data.yggdrasilToken.extra["userName"] = username; account->data.yggdrasilToken.extra["userName"] = username;
account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]")); account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]"));
account->data.minecraftEntitlement.ownsMinecraft = true; account->data.minecraftEntitlement.ownsMinecraft = true;
account->data.minecraftEntitlement.canPlayMinecraft = true; account->data.minecraftEntitlement.canPlayMinecraft = true;
account->data.minecraftProfile.id = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]")); account->data.minecraftProfile.id = uuidFromUsername(username).toString().remove(QRegularExpression("[{}-]"));
account->data.minecraftProfile.name = username; account->data.minecraftProfile.name = username;
account->data.minecraftProfile.validity = Katabasis::Validity::Certain; account->data.minecraftProfile.validity = Katabasis::Validity::Certain;
return account; return account;
@ -334,3 +335,32 @@ void MinecraftAccount::incrementUses()
qWarning() << "Profile" << data.profileId() << "is now in use."; qWarning() << "Profile" << data.profileId() << "is now in use.";
} }
} }
QUuid MinecraftAccount::uuidFromUsername(QString username) {
auto input = QString("OfflinePlayer:%1").arg(username).toUtf8();
// basically a reimplementation of Java's UUID#nameUUIDFromBytes
QByteArray digest = QCryptographicHash::hash(input, QCryptographicHash::Md5);
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
auto bOr = [](QByteArray& array, int index, char value) {
array[index] = array.at(index) | value;
};
auto bAnd = [](QByteArray& array, int index, char value) {
array[index] = array.at(index) & value;
};
#else
auto bOr = [](QByteArray& array, qsizetype index, char value) {
array[index] |= value;
};
auto bAnd = [](QByteArray& array, qsizetype index, char value) {
array[index] &= value;
};
#endif
bAnd(digest, 6, (char) 0x0f); // clear version
bOr(digest, 6, (char) 0x30); // set to version 3
bAnd(digest, 8, (char) 0x3f); // clear variant
bOr(digest, 8, (char) 0x80); // set to IETF variant
return QUuid::fromRfc4122(digest);
}

View File

@ -98,6 +98,8 @@ public: /* construction */
static MinecraftAccountPtr loadFromJsonV2(const QJsonObject &json); static MinecraftAccountPtr loadFromJsonV2(const QJsonObject &json);
static MinecraftAccountPtr loadFromJsonV3(const QJsonObject &json); static MinecraftAccountPtr loadFromJsonV3(const QJsonObject &json);
static QUuid uuidFromUsername(QString username);
//! Saves a MinecraftAccount to a JSON object and returns it. //! Saves a MinecraftAccount to a JSON object and returns it.
QJsonObject saveToJson() const; QJsonObject saveToJson() const;

View File

@ -273,6 +273,7 @@ void Yggdrasil::processReply() {
AccountTaskState::STATE_FAILED_GONE, AccountTaskState::STATE_FAILED_GONE,
tr("The Mojang account no longer exists. It may have been migrated to a Microsoft account.") tr("The Mojang account no longer exists. It may have been migrated to a Microsoft account.")
); );
return;
} }
default: default:
changeState( changeState(

View File

@ -74,6 +74,7 @@ std::pair<int, bool> DataPack::compare(const Resource& other, SortType type) con
auto res = Resource::compare(other, type); auto res = Resource::compare(other, type);
if (res.first != 0) if (res.first != 0)
return res; return res;
break;
} }
case SortType::PACK_FORMAT: { case SortType::PACK_FORMAT: {
auto this_ver = packFormat(); auto this_ver = packFormat();
@ -83,6 +84,7 @@ std::pair<int, bool> DataPack::compare(const Resource& other, SortType type) con
return { 1, type == SortType::PACK_FORMAT }; return { 1, type == SortType::PACK_FORMAT };
if (this_ver < other_ver) if (this_ver < other_ver)
return { -1, type == SortType::PACK_FORMAT }; return { -1, type == SortType::PACK_FORMAT };
break;
} }
} }
return { 0, false }; return { 0, false };

View File

@ -91,6 +91,7 @@ std::pair<int, bool> Mod::compare(const Resource& other, SortType type) const
auto res = Resource::compare(other, type); auto res = Resource::compare(other, type);
if (res.first != 0) if (res.first != 0)
return res; return res;
break;
} }
case SortType::VERSION: { case SortType::VERSION: {
auto this_ver = Version(version()); auto this_ver = Version(version());
@ -99,11 +100,13 @@ std::pair<int, bool> Mod::compare(const Resource& other, SortType type) const
return { 1, type == SortType::VERSION }; return { 1, type == SortType::VERSION };
if (this_ver < other_ver) if (this_ver < other_ver)
return { -1, type == SortType::VERSION }; return { -1, type == SortType::VERSION };
break;
} }
case SortType::PROVIDER: { case SortType::PROVIDER: {
auto compare_result = QString::compare(provider().value_or("Unknown"), cast_other->provider().value_or("Unknown"), Qt::CaseInsensitive); auto compare_result = QString::compare(provider().value_or("Unknown"), cast_other->provider().value_or("Unknown"), Qt::CaseInsensitive);
if (compare_result != 0) if (compare_result != 0)
return { compare_result, type == SortType::PROVIDER }; return { compare_result, type == SortType::PROVIDER };
break;
} }
} }
return { 0, false }; return { 0, false };
@ -123,7 +126,7 @@ bool Mod::applyFilter(QRegularExpression filter) const
return Resource::applyFilter(filter); return Resource::applyFilter(filter);
} }
auto Mod::destroy(QDir& index_dir, bool preserve_metadata) -> bool auto Mod::destroy(QDir& index_dir, bool preserve_metadata, bool attempt_trash) -> bool
{ {
if (!preserve_metadata) { if (!preserve_metadata) {
qDebug() << QString("Destroying metadata for '%1' on purpose").arg(name()); qDebug() << QString("Destroying metadata for '%1' on purpose").arg(name());
@ -136,7 +139,7 @@ auto Mod::destroy(QDir& index_dir, bool preserve_metadata) -> bool
} }
} }
return Resource::destroy(); return Resource::destroy(attempt_trash);
} }
auto Mod::details() const -> const ModDetails& auto Mod::details() const -> const ModDetails&
@ -166,6 +169,13 @@ auto Mod::homeurl() const -> QString
return details().homeurl; return details().homeurl;
} }
auto Mod::metaurl() const -> QString
{
if (metadata() == nullptr)
return homeurl();
return ModPlatform::getMetaURL(metadata()->provider, metadata()->project_id);
}
auto Mod::description() const -> QString auto Mod::description() const -> QString
{ {
return details().description; return details().description;

View File

@ -70,6 +70,7 @@ public:
auto provider() const -> std::optional<QString>; auto provider() const -> std::optional<QString>;
auto licenses() const -> const QList<ModLicense>&; auto licenses() const -> const QList<ModLicense>&;
auto issueTracker() const -> QString; auto issueTracker() const -> QString;
auto metaurl() const -> QString;
/** Get the intneral path to the mod's icon file*/ /** Get the intneral path to the mod's icon file*/
QString iconPath() const { return m_local_details.icon_file; }; QString iconPath() const { return m_local_details.icon_file; };
@ -92,7 +93,7 @@ public:
[[nodiscard]] bool applyFilter(QRegularExpression filter) const override; [[nodiscard]] bool applyFilter(QRegularExpression filter) const override;
// Delete all the files of this mod // Delete all the files of this mod
auto destroy(QDir& index_dir, bool preserve_metadata = false) -> bool; auto destroy(QDir& index_dir, bool preserve_metadata = false, bool attempt_trash = true) -> bool;
void finishResolvingWithDetails(ModDetails&& details); void finishResolvingWithDetails(ModDetails&& details);

View File

@ -202,7 +202,7 @@ bool ModFolderModel::uninstallMod(const QString& filename, bool preserve_metadat
for(auto mod : allMods()) { for(auto mod : allMods()) {
if(mod->fileinfo().fileName() == filename) { if(mod->fileinfo().fileName() == filename) {
auto index_dir = indexDir(); auto index_dir = indexDir();
mod->destroy(index_dir, preserve_metadata); mod->destroy(index_dir, preserve_metadata, false);
update(); update();
@ -215,15 +215,10 @@ bool ModFolderModel::uninstallMod(const QString& filename, bool preserve_metadat
bool ModFolderModel::deleteMods(const QModelIndexList& indexes) bool ModFolderModel::deleteMods(const QModelIndexList& indexes)
{ {
if(!m_can_interact) {
return false;
}
if (indexes.isEmpty()) if (indexes.isEmpty())
return true; return true;
for (auto i: indexes) for (auto i : indexes) {
{
if (i.column() != 0) { if (i.column() != 0) {
continue; continue;
} }

View File

@ -71,6 +71,7 @@ std::pair<int, bool> Resource::compare(const Resource& other, SortType type) con
return { 1, type == SortType::ENABLED }; return { 1, type == SortType::ENABLED };
if (!enabled() && other.enabled()) if (!enabled() && other.enabled())
return { -1, type == SortType::ENABLED }; return { -1, type == SortType::ENABLED };
break;
case SortType::NAME: { case SortType::NAME: {
QString this_name{ name() }; QString this_name{ name() };
QString other_name{ other.name() }; QString other_name{ other.name() };
@ -81,12 +82,14 @@ std::pair<int, bool> Resource::compare(const Resource& other, SortType type) con
auto compare_result = QString::compare(this_name, other_name, Qt::CaseInsensitive); auto compare_result = QString::compare(this_name, other_name, Qt::CaseInsensitive);
if (compare_result != 0) if (compare_result != 0)
return { compare_result, type == SortType::NAME }; return { compare_result, type == SortType::NAME };
break;
} }
case SortType::DATE: case SortType::DATE:
if (dateTimeChanged() > other.dateTimeChanged()) if (dateTimeChanged() > other.dateTimeChanged())
return { 1, type == SortType::DATE }; return { 1, type == SortType::DATE };
if (dateTimeChanged() < other.dateTimeChanged()) if (dateTimeChanged() < other.dateTimeChanged())
return { -1, type == SortType::DATE }; return { -1, type == SortType::DATE };
break;
} }
return { 0, false }; return { 0, false };
@ -145,14 +148,10 @@ bool Resource::enable(EnableAction action)
return true; return true;
} }
bool Resource::destroy() bool Resource::destroy(bool attemptTrash)
{ {
m_type = ResourceType::UNKNOWN; m_type = ResourceType::UNKNOWN;
return (attemptTrash && FS::trash(m_file_info.filePath())) || FS::deletePath(m_file_info.filePath());
if (FS::trash(m_file_info.filePath()))
return true;
return FS::deletePath(m_file_info.filePath());
} }
bool Resource::isSymLinkUnder(const QString& instPath) const bool Resource::isSymLinkUnder(const QString& instPath) const

View File

@ -92,7 +92,7 @@ class Resource : public QObject {
} }
// Delete all files of this resource. // Delete all files of this resource.
bool destroy(); bool destroy(bool attemptTrash = true);
[[nodiscard]] auto isSymLink() const -> bool { return m_file_info.isSymLink(); } [[nodiscard]] auto isSymLink() const -> bool { return m_file_info.isSymLink(); }

View File

@ -1,14 +1,15 @@
#include "ResourceFolderModel.h" #include "ResourceFolderModel.h"
#include <QMessageBox>
#include <QCoreApplication> #include <QCoreApplication>
#include <QDebug> #include <QDebug>
#include <QFileInfo> #include <QFileInfo>
#include <QIcon> #include <QIcon>
#include <QMenu>
#include <QMimeData> #include <QMimeData>
#include <QStyle> #include <QStyle>
#include <QThreadPool> #include <QThreadPool>
#include <QUrl> #include <QUrl>
#include <QMenu>
#include "Application.h" #include "Application.h"
#include "FileSystem.h" #include "FileSystem.h"
@ -18,6 +19,7 @@
#include "settings/Setting.h" #include "settings/Setting.h"
#include "tasks/Task.h" #include "tasks/Task.h"
#include "ui/dialogs/CustomMessageBox.h"
ResourceFolderModel::ResourceFolderModel(QDir dir, BaseInstance* instance, QObject* parent, bool create_dir) ResourceFolderModel::ResourceFolderModel(QDir dir, BaseInstance* instance, QObject* parent, bool create_dir)
: QAbstractListModel(parent), m_dir(dir), m_instance(instance), m_watcher(this) : QAbstractListModel(parent), m_dir(dir), m_instance(instance), m_watcher(this)
@ -77,10 +79,6 @@ bool ResourceFolderModel::stopWatching(const QStringList paths)
bool ResourceFolderModel::installResource(QString original_path) bool ResourceFolderModel::installResource(QString original_path)
{ {
if (!m_can_interact) {
return false;
}
// NOTE: fix for GH-1178: remove trailing slash to avoid issues with using the empty result of QFileInfo::fileName // NOTE: fix for GH-1178: remove trailing slash to avoid issues with using the empty result of QFileInfo::fileName
original_path = FS::NormalizePath(original_path); original_path = FS::NormalizePath(original_path);
QFileInfo file_info(original_path); QFileInfo file_info(original_path);
@ -159,7 +157,7 @@ bool ResourceFolderModel::uninstallResource(QString file_name)
{ {
for (auto& resource : m_resources) { for (auto& resource : m_resources) {
if (resource->fileinfo().fileName() == file_name) { if (resource->fileinfo().fileName() == file_name) {
auto res = resource->destroy(); auto res = resource->destroy(false);
update(); update();
@ -171,9 +169,6 @@ bool ResourceFolderModel::uninstallResource(QString file_name)
bool ResourceFolderModel::deleteResources(const QModelIndexList& indexes) bool ResourceFolderModel::deleteResources(const QModelIndexList& indexes)
{ {
if (!m_can_interact)
return false;
if (indexes.isEmpty()) if (indexes.isEmpty())
return true; return true;
@ -194,9 +189,6 @@ bool ResourceFolderModel::deleteResources(const QModelIndexList& indexes)
bool ResourceFolderModel::setResourceEnabled(const QModelIndexList& indexes, EnableAction action) bool ResourceFolderModel::setResourceEnabled(const QModelIndexList& indexes, EnableAction action)
{ {
if (!m_can_interact)
return false;
if (indexes.isEmpty()) if (indexes.isEmpty())
return true; return true;
@ -249,7 +241,9 @@ bool ResourceFolderModel::update()
connect(m_current_update_task.get(), &Task::succeeded, this, &ResourceFolderModel::onUpdateSucceeded, connect(m_current_update_task.get(), &Task::succeeded, this, &ResourceFolderModel::onUpdateSucceeded,
Qt::ConnectionType::QueuedConnection); Qt::ConnectionType::QueuedConnection);
connect(m_current_update_task.get(), &Task::failed, this, &ResourceFolderModel::onUpdateFailed, Qt::ConnectionType::QueuedConnection); connect(m_current_update_task.get(), &Task::failed, this, &ResourceFolderModel::onUpdateFailed, Qt::ConnectionType::QueuedConnection);
connect(m_current_update_task.get(), &Task::finished, this, [=] { connect(
m_current_update_task.get(), &Task::finished, this,
[=] {
m_current_update_task.reset(); m_current_update_task.reset();
if (m_scheduled_update) { if (m_scheduled_update) {
m_scheduled_update = false; m_scheduled_update = false;
@ -257,7 +251,8 @@ bool ResourceFolderModel::update()
} else { } else {
emit updateFinished(); emit updateFinished();
} }
}, Qt::ConnectionType::QueuedConnection); },
Qt::ConnectionType::QueuedConnection);
QThreadPool::globalInstance()->start(m_current_update_task.get()); QThreadPool::globalInstance()->start(m_current_update_task.get());
@ -347,15 +342,9 @@ Qt::DropActions ResourceFolderModel::supportedDropActions() const
Qt::ItemFlags ResourceFolderModel::flags(const QModelIndex& index) const Qt::ItemFlags ResourceFolderModel::flags(const QModelIndex& index) const
{ {
Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index); Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index);
auto flags = defaultFlags; auto flags = defaultFlags | Qt::ItemIsDropEnabled;
if (!m_can_interact) { if (index.isValid())
flags &= ~Qt::ItemIsDropEnabled;
} else {
flags |= Qt::ItemIsDropEnabled;
if (index.isValid()) {
flags |= Qt::ItemIsUserCheckable; flags |= Qt::ItemIsUserCheckable;
}
}
return flags; return flags;
} }
@ -430,7 +419,8 @@ QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const
return m_resources[row]->internal_id() + return m_resources[row]->internal_id() +
tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original." tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original."
"\nCanonical Path: %1") "\nCanonical Path: %1")
.arg(at(row).fileinfo().canonicalFilePath());; .arg(at(row).fileinfo().canonicalFilePath());
;
} }
if (at(row).isMoreThanOneHardLink()) { if (at(row).isMoreThanOneHardLink()) {
return m_resources[row]->internal_id() + return m_resources[row]->internal_id() +
@ -463,8 +453,20 @@ bool ResourceFolderModel::setData(const QModelIndex& index, const QVariant& valu
if (row < 0 || row >= rowCount(index.parent()) || !index.isValid()) if (row < 0 || row >= rowCount(index.parent()) || !index.isValid())
return false; return false;
if (role == Qt::CheckStateRole) if (role == Qt::CheckStateRole) {
if (m_instance != nullptr && m_instance->isRunning()) {
auto response =
CustomMessageBox::selectable(nullptr, "Confirm toggle",
"If you enable/disable this resource while the game is running it may crash your game.\n"
"Are you sure you want to do this?",
QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No)
->exec();
if (response != QMessageBox::Yes)
return false;
}
return setResourceEnabled({ index }, EnableAction::TOGGLE); return setResourceEnabled({ index }, EnableAction::TOGGLE);
}
return false; return false;
} }
@ -583,16 +585,6 @@ SortType ResourceFolderModel::columnToSortKey(size_t column) const
return m_column_sort_keys.at(column); return m_column_sort_keys.at(column);
} }
void ResourceFolderModel::enableInteraction(bool enabled)
{
if (m_can_interact == enabled)
return;
m_can_interact = enabled;
if (size())
emit dataChanged(index(0), index(size() - 1));
}
/* Standard Proxy Model for createFilterProxyModel */ /* Standard Proxy Model for createFilterProxyModel */
[[nodiscard]] bool ResourceFolderModel::ProxyModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const [[nodiscard]] bool ResourceFolderModel::ProxyModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const
{ {
@ -628,6 +620,7 @@ void ResourceFolderModel::enableInteraction(bool enabled)
return (compare_result.first > 0); return (compare_result.first > 0);
} }
QString ResourceFolderModel::instDirPath() const { QString ResourceFolderModel::instDirPath() const
{
return QFileInfo(m_instance->instanceRoot()).absoluteFilePath(); return QFileInfo(m_instance->instanceRoot()).absoluteFilePath();
} }

View File

@ -14,8 +14,8 @@
#include "BaseInstance.h" #include "BaseInstance.h"
#include "tasks/Task.h"
#include "tasks/ConcurrentTask.h" #include "tasks/ConcurrentTask.h"
#include "tasks/Task.h"
class QSortFilterProxyModel; class QSortFilterProxyModel;
@ -141,10 +141,6 @@ class ResourceFolderModel : public QAbstractListModel {
QString instDirPath() const; QString instDirPath() const;
public slots:
void enableInteraction(bool enabled);
void disableInteraction(bool disabled) { enableInteraction(!disabled); }
signals: signals:
void updateFinished(); void updateFinished();
@ -193,7 +189,11 @@ class ResourceFolderModel : public QAbstractListModel {
* if the resource is complex and has more stuff to parse. * if the resource is complex and has more stuff to parse.
*/ */
virtual void onParseSucceeded(int ticket, QString resource_id); virtual void onParseSucceeded(int ticket, QString resource_id);
virtual void onParseFailed(int ticket, QString resource_id) { Q_UNUSED(ticket); Q_UNUSED(resource_id); } virtual void onParseFailed(int ticket, QString resource_id)
{
Q_UNUSED(ticket);
Q_UNUSED(resource_id);
}
protected: protected:
// Represents the relationship between a column's index (represented by the list index), and it's sorting key. // Represents the relationship between a column's index (represented by the list index), and it's sorting key.
@ -203,8 +203,6 @@ class ResourceFolderModel : public QAbstractListModel {
QStringList m_column_names_translated = {tr("Enable"), tr("Name"), tr("Last Modified")}; QStringList m_column_names_translated = {tr("Enable"), tr("Name"), tr("Last Modified")};
QList<QHeaderView::ResizeMode> m_column_resize_modes = { QHeaderView::ResizeToContents, QHeaderView::Stretch, QHeaderView::ResizeToContents }; QList<QHeaderView::ResizeMode> m_column_resize_modes = { QHeaderView::ResizeToContents, QHeaderView::Stretch, QHeaderView::ResizeToContents };
bool m_can_interact = true;
QDir m_dir; QDir m_dir;
BaseInstance* m_instance; BaseInstance* m_instance;
QFileSystemWatcher m_watcher; QFileSystemWatcher m_watcher;

View File

@ -102,6 +102,7 @@ std::pair<int, bool> ResourcePack::compare(const Resource& other, SortType type)
auto res = Resource::compare(other, type); auto res = Resource::compare(other, type);
if (res.first != 0) if (res.first != 0)
return res; return res;
break;
} }
case SortType::PACK_FORMAT: { case SortType::PACK_FORMAT: {
auto this_ver = packFormat(); auto this_ver = packFormat();
@ -111,6 +112,7 @@ std::pair<int, bool> ResourcePack::compare(const Resource& other, SortType type)
return { 1, type == SortType::PACK_FORMAT }; return { 1, type == SortType::PACK_FORMAT };
if (this_ver < other_ver) if (this_ver < other_ver)
return { -1, type == SortType::PACK_FORMAT }; return { -1, type == SortType::PACK_FORMAT };
break;
} }
} }
return { 0, false }; return { 0, false };

View File

@ -44,7 +44,11 @@ static const QMap<PackedResourceType, QString> s_packed_type_names = {
namespace ResourceUtils { namespace ResourceUtils {
PackedResourceType identify(QFileInfo file){ PackedResourceType identify(QFileInfo file){
if (file.exists() && file.isFile()) { if (file.exists() && file.isFile()) {
if (ResourcePackUtils::validate(file)) { if (ModUtils::validate(file)) {
// mods can contain resource and data packs so they must be tested first
qDebug() << file.fileName() << "is a mod";
return PackedResourceType::Mod;
} else if (ResourcePackUtils::validate(file)) {
qDebug() << file.fileName() << "is a resource pack"; qDebug() << file.fileName() << "is a resource pack";
return PackedResourceType::ResourcePack; return PackedResourceType::ResourcePack;
} else if (TexturePackUtils::validate(file)) { } else if (TexturePackUtils::validate(file)) {
@ -53,9 +57,6 @@ PackedResourceType identify(QFileInfo file){
} else if (DataPackUtils::validate(file)) { } else if (DataPackUtils::validate(file)) {
qDebug() << file.fileName() << "is a data pack"; qDebug() << file.fileName() << "is a data pack";
return PackedResourceType::DataPack; return PackedResourceType::DataPack;
} else if (ModUtils::validate(file)) {
qDebug() << file.fileName() << "is a mod";
return PackedResourceType::Mod;
} else if (WorldSaveUtils::validate(file)) { } else if (WorldSaveUtils::validate(file)) {
qDebug() << file.fileName() << "is a world save"; qDebug() << file.fileName() << "is a world save";
return PackedResourceType::WorldSave; return PackedResourceType::WorldSave;

View File

@ -103,7 +103,7 @@ void ModFolderLoadTask::executeTask()
while (iter.hasNext()) { while (iter.hasNext()) {
auto mod = iter.next().value(); auto mod = iter.next().value();
if (mod->status() == ModStatus::NotInstalled) { if (mod->status() == ModStatus::NotInstalled) {
mod->destroy(m_index_dir, false); mod->destroy(m_index_dir, false, false);
iter.remove(); iter.remove();
} }
} }

View File

@ -45,7 +45,7 @@ void AssetUpdateTask::executeTask()
connect(downloadJob.get(), &NetJob::failed, this, &AssetUpdateTask::assetIndexFailed); connect(downloadJob.get(), &NetJob::failed, this, &AssetUpdateTask::assetIndexFailed);
connect(downloadJob.get(), &NetJob::aborted, this, [this]{ emitFailed(tr("Aborted")); }); connect(downloadJob.get(), &NetJob::aborted, this, [this]{ emitFailed(tr("Aborted")); });
connect(downloadJob.get(), &NetJob::progress, this, &AssetUpdateTask::progress); connect(downloadJob.get(), &NetJob::progress, this, &AssetUpdateTask::progress);
connect(downloadJob.get(), &NetJob::stepProgress, this, &AssetUpdateTask::propogateStepProgress); connect(downloadJob.get(), &NetJob::stepProgress, this, &AssetUpdateTask::propagateStepProgress);
qDebug() << m_inst->name() << ": Starting asset index download"; qDebug() << m_inst->name() << ": Starting asset index download";
downloadJob->start(); downloadJob->start();
@ -84,7 +84,7 @@ void AssetUpdateTask::assetIndexFinished()
connect(downloadJob.get(), &NetJob::failed, this, &AssetUpdateTask::assetsFailed); connect(downloadJob.get(), &NetJob::failed, this, &AssetUpdateTask::assetsFailed);
connect(downloadJob.get(), &NetJob::aborted, this, [this]{ emitFailed(tr("Aborted")); }); connect(downloadJob.get(), &NetJob::aborted, this, [this]{ emitFailed(tr("Aborted")); });
connect(downloadJob.get(), &NetJob::progress, this, &AssetUpdateTask::progress); connect(downloadJob.get(), &NetJob::progress, this, &AssetUpdateTask::progress);
connect(downloadJob.get(), &NetJob::stepProgress, this, &AssetUpdateTask::propogateStepProgress); connect(downloadJob.get(), &NetJob::stepProgress, this, &AssetUpdateTask::propagateStepProgress);
downloadJob->start(); downloadJob->start();
return; return;
} }

View File

@ -75,7 +75,7 @@ void FMLLibrariesTask::executeTask()
connect(dljob.get(), &NetJob::failed, this, &FMLLibrariesTask::fmllibsFailed); connect(dljob.get(), &NetJob::failed, this, &FMLLibrariesTask::fmllibsFailed);
connect(dljob.get(), &NetJob::aborted, this, [this]{ emitFailed(tr("Aborted")); }); connect(dljob.get(), &NetJob::aborted, this, [this]{ emitFailed(tr("Aborted")); });
connect(dljob.get(), &NetJob::progress, this, &FMLLibrariesTask::progress); connect(dljob.get(), &NetJob::progress, this, &FMLLibrariesTask::progress);
connect(dljob.get(), &NetJob::stepProgress, this, &FMLLibrariesTask::propogateStepProgress); connect(dljob.get(), &NetJob::stepProgress, this, &FMLLibrariesTask::propagateStepProgress);
downloadJob.reset(dljob); downloadJob.reset(dljob);
downloadJob->start(); downloadJob->start();
} }

View File

@ -70,7 +70,7 @@ void LibrariesTask::executeTask()
connect(downloadJob.get(), &NetJob::failed, this, &LibrariesTask::jarlibFailed); connect(downloadJob.get(), &NetJob::failed, this, &LibrariesTask::jarlibFailed);
connect(downloadJob.get(), &NetJob::aborted, this, [this]{ emitFailed(tr("Aborted")); }); connect(downloadJob.get(), &NetJob::aborted, this, [this]{ emitFailed(tr("Aborted")); });
connect(downloadJob.get(), &NetJob::progress, this, &LibrariesTask::progress); connect(downloadJob.get(), &NetJob::progress, this, &LibrariesTask::progress);
connect(downloadJob.get(), &NetJob::stepProgress, this, &LibrariesTask::propogateStepProgress); connect(downloadJob.get(), &NetJob::stepProgress, this, &LibrariesTask::propagateStepProgress);
downloadJob->start(); downloadJob->start();
} }

View File

@ -145,7 +145,8 @@ void EnsureMetadataTask::executeTask()
connect(project_task.get(), &Task::finished, this, [=] { connect(project_task.get(), &Task::finished, this, [=] {
invalidade_leftover(); invalidade_leftover();
project_task->deleteLater(); project_task->deleteLater();
m_current_task = nullptr; if (m_current_task)
m_current_task.reset();
}); });
m_current_task = project_task; m_current_task = project_task;
@ -154,7 +155,8 @@ void EnsureMetadataTask::executeTask()
connect(version_task.get(), &Task::finished, [=] { connect(version_task.get(), &Task::finished, [=] {
version_task->deleteLater(); version_task->deleteLater();
m_current_task = nullptr; if (m_current_task)
m_current_task.reset();
}); });
if (m_mods.size() > 1) if (m_mods.size() > 1)

View File

@ -77,4 +77,10 @@ auto ProviderCapabilities::hash(ResourceProvider p, QIODevice* device, QString t
return { hash.result().toHex() }; return { hash.result().toHex() };
} }
QString getMetaURL(ResourceProvider provider, QVariant projectID)
{
return ((provider == ModPlatform::ResourceProvider::FLAME) ? "https://www.curseforge.com/projects/" : "https://modrinth.com/mod/") +
projectID.toString();
}
} // namespace ModPlatform } // namespace ModPlatform

View File

@ -128,6 +128,7 @@ struct IndexedPack {
return std::any_of(versions.constBegin(), versions.constEnd(), [](auto const& v) { return v.is_currently_selected; }); return std::any_of(versions.constBegin(), versions.constEnd(), [](auto const& v) { return v.is_currently_selected; });
} }
}; };
QString getMetaURL(ResourceProvider provider, QVariant projectID);
struct OverrideDep { struct OverrideDep {
QString quilt; QString quilt;
@ -144,6 +145,7 @@ inline auto getOverrideDeps() -> QList<OverrideDep>
{ "qvIfYCYJ", "P7dR8mSH", "API", ModPlatform::ResourceProvider::MODRINTH }, { "qvIfYCYJ", "P7dR8mSH", "API", ModPlatform::ResourceProvider::MODRINTH },
{ "lwVhp9o5", "Ha28R6CL", "KotlinLibraries", ModPlatform::ResourceProvider::MODRINTH } }; { "lwVhp9o5", "Ha28R6CL", "KotlinLibraries", ModPlatform::ResourceProvider::MODRINTH } };
}; };
QString getMetaURL(ResourceProvider provider, QVariant projectID);
} // namespace ModPlatform } // namespace ModPlatform

View File

@ -684,7 +684,7 @@ void PackInstallTask::installConfigs()
abortable = true; abortable = true;
setProgress(current, total); setProgress(current, total);
}); });
connect(jobPtr.get(), &NetJob::stepProgress, this, &PackInstallTask::propogateStepProgress); connect(jobPtr.get(), &NetJob::stepProgress, this, &PackInstallTask::propagateStepProgress);
connect(jobPtr.get(), &NetJob::aborted, [&]{ connect(jobPtr.get(), &NetJob::aborted, [&]{
abortable = false; abortable = false;
jobPtr.reset(); jobPtr.reset();
@ -852,7 +852,7 @@ void PackInstallTask::downloadMods()
abortable = true; abortable = true;
setProgress(current, total); setProgress(current, total);
}); });
connect(jobPtr.get(), &NetJob::stepProgress, this, &PackInstallTask::propogateStepProgress); connect(jobPtr.get(), &NetJob::stepProgress, this, &PackInstallTask::propagateStepProgress);
connect(jobPtr.get(), &NetJob::aborted, [&] connect(jobPtr.get(), &NetJob::aborted, [&]
{ {
abortable = false; abortable = false;

View File

@ -21,6 +21,10 @@ bool Flame::FileResolvingTask::abort()
void Flame::FileResolvingTask::executeTask() void Flame::FileResolvingTask::executeTask()
{ {
if (m_toProcess.files.isEmpty()) { // no file to resolve so leave it empty and emit success immediately
emitSucceeded();
return;
}
setStatus(tr("Resolving mod IDs...")); setStatus(tr("Resolving mod IDs..."));
setProgress(0, 3); setProgress(0, 3);
m_dljob.reset(new NetJob("Mod id resolver", m_network)); m_dljob.reset(new NetJob("Mod id resolver", m_network));
@ -48,7 +52,7 @@ void Flame::FileResolvingTask::executeTask()
stepProgress(*step_progress); stepProgress(*step_progress);
emitFailed(reason); emitFailed(reason);
}); });
connect(m_dljob.get(), &NetJob::stepProgress, this, &FileResolvingTask::propogateStepProgress); connect(m_dljob.get(), &NetJob::stepProgress, this, &FileResolvingTask::propagateStepProgress);
connect(m_dljob.get(), &NetJob::progress, this, [this, step_progress](qint64 current, qint64 total) { connect(m_dljob.get(), &NetJob::progress, this, [this, step_progress](qint64 current, qint64 total) {
qDebug() << "Resolve slug progress" << current << total; qDebug() << "Resolve slug progress" << current << total;
step_progress->update(current, total); step_progress->update(current, total);
@ -114,7 +118,7 @@ void Flame::FileResolvingTask::netJobFinished()
stepProgress(*step_progress); stepProgress(*step_progress);
emitFailed(reason); emitFailed(reason);
}); });
connect(m_checkJob.get(), &NetJob::stepProgress, this, &FileResolvingTask::propogateStepProgress); connect(m_checkJob.get(), &NetJob::stepProgress, this, &FileResolvingTask::propagateStepProgress);
connect(m_checkJob.get(), &NetJob::progress, this, [this, step_progress](qint64 current, qint64 total) { connect(m_checkJob.get(), &NetJob::progress, this, [this, step_progress](qint64 current, qint64 total) {
qDebug() << "Resolve slug progress" << current << total; qDebug() << "Resolve slug progress" << current << total;
step_progress->update(current, total); step_progress->update(current, total);
@ -128,7 +132,8 @@ void Flame::FileResolvingTask::netJobFinished()
m_checkJob->start(); m_checkJob->start();
} }
void Flame::FileResolvingTask::modrinthCheckFinished() { void Flame::FileResolvingTask::modrinthCheckFinished()
{
setProgress(2, 3); setProgress(2, 3);
qDebug() << "Finished with blocked mods : " << blockedProjects.size(); qDebug() << "Finished with blocked mods : " << blockedProjects.size();
@ -156,9 +161,7 @@ void Flame::FileResolvingTask::modrinthCheckFinished() {
// copy to an output list and filter out projects found on modrinth // copy to an output list and filter out projects found on modrinth
auto block = std::make_shared<QList<File*>>(); auto block = std::make_shared<QList<File*>>();
auto it = blockedProjects.keys(); auto it = blockedProjects.keys();
std::copy_if(it.begin(), it.end(), std::back_inserter(*block), [](File *f) { std::copy_if(it.begin(), it.end(), std::back_inserter(*block), [](File* f) { return !f->resolved; });
return !f->resolved;
});
// Display not found mods early // Display not found mods early
if (!block->empty()) { if (!block->empty()) {
// blocked mods found, we need the slug for displaying.... we need another job :D ! // blocked mods found, we need the slug for displaying.... we need another job :D !
@ -173,8 +176,8 @@ void Flame::FileResolvingTask::modrinthCheckFinished() {
QObject::connect(dl.get(), &Net::Download::succeeded, [block, index, output]() { QObject::connect(dl.get(), &Net::Download::succeeded, [block, index, output]() {
auto mod = block->at(index); // use the shared_ptr so it is captured and only freed when we are done auto mod = block->at(index); // use the shared_ptr so it is captured and only freed when we are done
auto json = QJsonDocument::fromJson(*output); auto json = QJsonDocument::fromJson(*output);
auto base = Json::requireString(Json::requireObject(Json::requireObject(Json::requireObject(json),"data"),"links"), auto base =
"websiteUrl"); Json::requireString(Json::requireObject(Json::requireObject(Json::requireObject(json), "data"), "links"), "websiteUrl");
auto link = QString("%1/download/%2").arg(base, QString::number(mod->fileId)); auto link = QString("%1/download/%2").arg(base, QString::number(mod->fileId));
mod->websiteUrl = link; mod->websiteUrl = link;
}); });
@ -192,7 +195,7 @@ void Flame::FileResolvingTask::modrinthCheckFinished() {
stepProgress(*step_progress); stepProgress(*step_progress);
emitFailed(reason); emitFailed(reason);
}); });
connect(m_slugJob.get(), &NetJob::stepProgress, this, &FileResolvingTask::propogateStepProgress); connect(m_slugJob.get(), &NetJob::stepProgress, this, &FileResolvingTask::propagateStepProgress);
connect(m_slugJob.get(), &NetJob::progress, this, [this, step_progress](qint64 current, qint64 total) { connect(m_slugJob.get(), &NetJob::progress, this, [this, step_progress](qint64 current, qint64 total) {
qDebug() << "Resolve slug progress" << current << total; qDebug() << "Resolve slug progress" << current << total;
step_progress->update(current, total); step_progress->update(current, total);

View File

@ -23,6 +23,8 @@ class FlameAPI : public NetworkResourceAPI {
[[nodiscard]] auto getSortingMethods() const -> QList<ResourceAPI::SortingMethod> override; [[nodiscard]] auto getSortingMethods() const -> QList<ResourceAPI::SortingMethod> override;
static inline auto validateModLoaders(ModLoaderTypes loaders) -> bool { return loaders & (Forge | Fabric | Quilt); }
private: private:
static int getClassId(ModPlatform::ResourceType type) static int getClassId(ModPlatform::ResourceType type)
{ {

View File

@ -57,15 +57,11 @@
#include <QDebug> #include <QDebug>
#include <QFileInfo> #include <QFileInfo>
#include "meta/Index.h"
#include "meta/VersionList.h"
#include "minecraft/World.h" #include "minecraft/World.h"
#include "minecraft/mod/tasks/LocalResourceParse.h" #include "minecraft/mod/tasks/LocalResourceParse.h"
const static QMap<QString, QString> forgemap = { { "1.2.5", "3.4.9.171" },
{ "1.4.2", "6.0.1.355" },
{ "1.4.7", "6.6.2.534" },
{ "1.5.2", "7.8.1.737" } };
static const FlameAPI api; static const FlameAPI api;
bool FlameCreationTask::abort() bool FlameCreationTask::abort()
@ -259,6 +255,56 @@ bool FlameCreationTask::updateInstance()
return false; return false;
} }
QString FlameCreationTask::getVersionForLoader(QString uid, QString loaderType, QString loaderVersion, QString mcVersion)
{
if (loaderVersion == "recommended") {
auto vlist = APPLICATION->metadataIndex()->get(uid);
if (!vlist) {
setError(tr("Failed to get local metadata index for %1").arg(uid));
return {};
}
if (!vlist->isLoaded()) {
QEventLoop loadVersionLoop;
auto task = vlist->getLoadTask();
connect(task.get(), &Task::finished, &loadVersionLoop, &QEventLoop::quit);
if (!task->isRunning())
task->start();
loadVersionLoop.exec();
}
for (auto version : vlist->versions()) {
// first recommended build we find, we use.
if (!version->isRecommended())
continue;
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
// filtering for those loaders.
if (loaderType == "forge") {
auto iter = std::find_if(reqs.begin(), reqs.end(), [mcVersion](const Meta::Require& req) {
return req.uid == "net.minecraft" && req.equalsVersion == mcVersion;
});
if (iter == reqs.end())
continue;
}
return version->descriptor();
}
setError(tr("Failed to find version for %1 loader").arg(loaderType));
return {};
}
if (loaderVersion.isEmpty()) {
emitFailed(tr("No loader version set for modpack!"));
return {};
}
return loaderVersion;
}
bool FlameCreationTask::createInstance() bool FlameCreationTask::createInstance()
{ {
QEventLoop loop; QEventLoop loop;
@ -297,22 +343,29 @@ bool FlameCreationTask::createInstance()
} }
} }
QString forgeVersion; QString loaderType;
QString fabricVersion; QString loaderUid;
// TODO: is Quilt relevant here? QString loaderVersion;
for (auto& loader : m_pack.minecraft.modLoaders) { for (auto& loader : m_pack.minecraft.modLoaders) {
auto id = loader.id; auto id = loader.id;
if (id.startsWith("forge-")) { if (id.startsWith("forge-")) {
id.remove("forge-"); id.remove("forge-");
forgeVersion = id; loaderType = "forge";
continue; loaderUid = "net.minecraftforge";
} } else if (loaderType == "fabric") {
if (id.startsWith("fabric-")) {
id.remove("fabric-"); id.remove("fabric-");
fabricVersion = id; loaderType = "fabric";
loaderUid = "net.fabricmc.fabric-loader";
} else if (loaderType == "quilt") {
id.remove("quilt-");
loaderType = "quilt";
loaderUid = "org.quiltmc.quilt-loader";
} else {
logWarning(tr("Unknown mod loader in manifest: %1").arg(id));
continue; continue;
} }
logWarning(tr("Unknown mod loader in manifest: %1").arg(id)); loaderVersion = id;
} }
QString configPath = FS::PathCombine(m_stagingPath, "instance.cfg"); QString configPath = FS::PathCombine(m_stagingPath, "instance.cfg");
@ -329,19 +382,12 @@ bool FlameCreationTask::createInstance()
auto components = instance.getPackProfile(); auto components = instance.getPackProfile();
components->buildingFromScratch(); components->buildingFromScratch();
components->setComponentVersion("net.minecraft", mcVersion, true); components->setComponentVersion("net.minecraft", mcVersion, true);
if (!forgeVersion.isEmpty()) { if (!loaderType.isEmpty()) {
// FIXME: dirty, nasty, hack. Proper solution requires dependency resolution and knowledge of the metadata. auto version = getVersionForLoader(loaderUid, loaderType, loaderVersion, mcVersion);
if (forgeVersion == "recommended") { if (version.isEmpty())
if (forgemap.contains(mcVersion)) { return false;
forgeVersion = forgemap[mcVersion]; components->setComponentVersion(loaderUid, version);
} else {
logWarning(tr("Could not map recommended Forge version for Minecraft %1").arg(mcVersion));
} }
}
components->setComponentVersion("net.minecraftforge", forgeVersion);
}
if (!fabricVersion.isEmpty())
components->setComponentVersion("net.fabricmc.fabric-loader", fabricVersion);
if (m_instIcon != "default") { if (m_instIcon != "default") {
instance.setIconKey(m_instIcon); instance.setIconKey(m_instIcon);
@ -386,7 +432,7 @@ bool FlameCreationTask::createInstance()
}); });
connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::progress, this, &FlameCreationTask::setProgress); connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::progress, this, &FlameCreationTask::setProgress);
connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::status, this, &FlameCreationTask::setStatus); connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::status, this, &FlameCreationTask::setStatus);
connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::stepProgress, this, &FlameCreationTask::propogateStepProgress); connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::stepProgress, this, &FlameCreationTask::propagateStepProgress);
connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::details, this, &FlameCreationTask::setDetails); connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::details, this, &FlameCreationTask::setDetails);
m_mod_id_resolver->start(); m_mod_id_resolver->start();
@ -470,8 +516,9 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop)
switch (result.type) { switch (result.type) {
case Flame::File::Type::Folder: { case Flame::File::Type::Folder: {
logWarning(tr("This 'Folder' may need extracting: %1").arg(relpath)); logWarning(tr("This 'Folder' may need extracting: %1").arg(relpath));
// fall-through intentional, we treat these as plain old mods and dump them wherever. // fallthrough intentional, we treat these as plain old mods and dump them wherever.
} }
/* fallthrough */
case Flame::File::Type::SingleFile: case Flame::File::Type::SingleFile:
case Flame::File::Type::Mod: { case Flame::File::Type::Mod: {
if (!result.url.isEmpty()) { if (!result.url.isEmpty()) {
@ -505,7 +552,7 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop)
setDetails(tr("%1 out of %2 complete").arg(current).arg(total)); setDetails(tr("%1 out of %2 complete").arg(current).arg(total));
setProgress(current, total); setProgress(current, total);
}); });
connect(m_files_job.get(), &NetJob::stepProgress, this, &FlameCreationTask::propogateStepProgress); connect(m_files_job.get(), &NetJob::stepProgress, this, &FlameCreationTask::propagateStepProgress);
connect(m_files_job.get(), &NetJob::finished, &loop, &QEventLoop::quit); connect(m_files_job.get(), &NetJob::finished, &loop, &QEventLoop::quit);
setStatus(tr("Downloading mods...")); setStatus(tr("Downloading mods..."));
@ -544,7 +591,6 @@ void FlameCreationTask::copyBlockedMods(QList<BlockedMod> const& blocked_mods)
setAbortable(true); setAbortable(true);
} }
void FlameCreationTask::validateZIPResouces() void FlameCreationTask::validateZIPResouces()
{ {
qDebug() << "Validating whether resources stored as .zip are in the right place"; qDebug() << "Validating whether resources stored as .zip are in the right place";
@ -562,6 +608,8 @@ void FlameCreationTask::validateZIPResouces()
if (FS::move(localPath, destPath)) { if (FS::move(localPath, destPath)) {
return destPath; return destPath;
} }
} else {
qDebug() << "Target folder of" << fileName << "is correct at" << targetFolder;
} }
return localPath; return localPath;
}; };
@ -583,6 +631,9 @@ void FlameCreationTask::validateZIPResouces()
QString worldPath; QString worldPath;
switch (type) { switch (type) {
case PackedResourceType::Mod:
validatePath(fileName, targetFolder, "mods");
break;
case PackedResourceType::ResourcePack: case PackedResourceType::ResourcePack:
validatePath(fileName, targetFolder, "resourcepacks"); validatePath(fileName, targetFolder, "resourcepacks");
break; break;
@ -592,9 +643,6 @@ void FlameCreationTask::validateZIPResouces()
case PackedResourceType::DataPack: case PackedResourceType::DataPack:
validatePath(fileName, targetFolder, "datapacks"); validatePath(fileName, targetFolder, "datapacks");
break; break;
case PackedResourceType::Mod :
validatePath(fileName, targetFolder, "mods");
break;
case PackedResourceType::ShaderPack: case PackedResourceType::ShaderPack:
// in theroy flame API can't do this but who knows, that *may* change ? // in theroy flame API can't do this but who knows, that *may* change ?
// better to handle it if it *does* occure in the future // better to handle it if it *does* occure in the future

View File

@ -57,10 +57,7 @@ class FlameCreationTask final : public InstanceCreationTask {
QString id, QString id,
QString version_id, QString version_id,
QString original_instance_id = {}) QString original_instance_id = {})
: InstanceCreationTask() : InstanceCreationTask(), m_parent(parent), m_managed_id(std::move(id)), m_managed_version_id(std::move(version_id))
, m_parent(parent)
, m_managed_id(std::move(id))
, m_managed_version_id(std::move(version_id))
{ {
setStagingPath(staging_path); setStagingPath(staging_path);
setParentSettings(global_settings); setParentSettings(global_settings);
@ -78,6 +75,7 @@ class FlameCreationTask final : public InstanceCreationTask {
void setupDownloadJob(QEventLoop&); void setupDownloadJob(QEventLoop&);
void copyBlockedMods(QList<BlockedMod> const& blocked_mods); void copyBlockedMods(QList<BlockedMod> const& blocked_mods);
void validateZIPResouces(); void validateZIPResouces();
QString getVersionForLoader(QString uid, QString loaderType, QString version, QString mcVersion);
private: private:
QWidget* m_parent = nullptr; QWidget* m_parent = nullptr;

View File

@ -0,0 +1,473 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "FlamePackExportTask.h"
#include <QJsonArray>
#include <QJsonObject>
#include <QCryptographicHash>
#include <QFileInfo>
#include <QMessageBox>
#include <QtConcurrentRun>
#include <algorithm>
#include <memory>
#include "Json.h"
#include "MMCZip.h"
#include "minecraft/PackProfile.h"
#include "minecraft/mod/ModFolderModel.h"
#include "modplatform/ModIndex.h"
#include "modplatform/flame/FlameModIndex.h"
#include "modplatform/helpers/HashUtils.h"
#include "tasks/Task.h"
const QString FlamePackExportTask::TEMPLATE = "<li><a href=\"{url}\">{name}{authors}</a></li>\n";
const QStringList FlamePackExportTask::FILE_EXTENSIONS({ "jar", "zip" });
FlamePackExportTask::FlamePackExportTask(const QString& name,
const QString& version,
const QString& author,
InstancePtr instance,
const QString& output,
MMCZip::FilterFunction filter)
: name(name)
, version(version)
, author(author)
, instance(instance)
, mcInstance(dynamic_cast<MinecraftInstance*>(instance.get()))
, gameRoot(instance->gameRoot())
, output(output)
, filter(filter)
{}
void FlamePackExportTask::executeTask()
{
setStatus(tr("Searching for files..."));
setProgress(0, 5);
collectFiles();
}
bool FlamePackExportTask::abort()
{
if (task != nullptr) {
task->abort();
task = nullptr;
emitAborted();
return true;
}
if (buildZipFuture.isRunning()) {
buildZipFuture.cancel();
// NOTE: Here we don't do `emitAborted()` because it will be done when `buildZipFuture` actually cancels, which may not occur
// immediately.
return true;
}
return false;
}
void FlamePackExportTask::collectFiles()
{
setAbortable(false);
QCoreApplication::processEvents();
files.clear();
if (!MMCZip::collectFileListRecursively(instance->gameRoot(), nullptr, &files, filter)) {
emitFailed(tr("Could not search for files"));
return;
}
pendingHashes.clear();
resolvedFiles.clear();
if (mcInstance != nullptr) {
mcInstance->loaderModList()->update();
connect(mcInstance->loaderModList().get(), &ModFolderModel::updateFinished, this, &FlamePackExportTask::collectHashes);
} else
collectHashes();
}
void FlamePackExportTask::collectHashes()
{
setAbortable(true);
setStatus(tr("Finding file hashes..."));
setProgress(1, 5);
auto allMods = mcInstance->loaderModList()->allMods();
ConcurrentTask::Ptr hashingTask(new ConcurrentTask(this, "MakeHashesTask", 10));
task.reset(hashingTask);
for (const QFileInfo& file : files) {
const QString relative = gameRoot.relativeFilePath(file.absoluteFilePath());
// require sensible file types
if (!std::any_of(FILE_EXTENSIONS.begin(), FILE_EXTENSIONS.end(), [&relative](const QString& extension) {
return relative.endsWith('.' + extension) || relative.endsWith('.' + extension + ".disabled");
}))
continue;
if (relative.startsWith("resourcepacks/") &&
(relative.endsWith(".zip") || relative.endsWith(".zip.disabled"))) { // is resourcepack
auto hashTask = Hashing::createFlameHasher(file.absoluteFilePath());
connect(hashTask.get(), &Hashing::Hasher::resultsReady, [this, relative, file](QString hash) {
if (m_state == Task::State::Running) {
pendingHashes.insert(hash, { relative, file.absoluteFilePath(), relative.endsWith(".zip") });
}
});
connect(hashTask.get(), &Task::failed, this, &FlamePackExportTask::emitFailed);
hashingTask->addTask(hashTask);
continue;
}
if (auto modIter = std::find_if(allMods.begin(), allMods.end(), [&file](Mod* mod) { return mod->fileinfo() == file; });
modIter != allMods.end()) {
const Mod* mod = *modIter;
if (!mod || mod->type() == ResourceType::FOLDER) {
continue;
}
if (mod->metadata() && mod->metadata()->provider == ModPlatform::ResourceProvider::FLAME) {
resolvedFiles.insert(mod->fileinfo().absoluteFilePath(),
{ mod->metadata()->project_id.toInt(), mod->metadata()->file_id.toInt(), mod->enabled(), true,
mod->metadata()->name, mod->metadata()->slug, mod->authors().join(", ") });
continue;
}
auto hashTask = Hashing::createFlameHasher(mod->fileinfo().absoluteFilePath());
connect(hashTask.get(), &Hashing::Hasher::resultsReady, [this, mod](QString hash) {
if (m_state == Task::State::Running) {
pendingHashes.insert(hash, { mod->name(), mod->fileinfo().absoluteFilePath(), mod->enabled(), true });
}
});
connect(hashTask.get(), &Task::failed, this, &FlamePackExportTask::emitFailed);
hashingTask->addTask(hashTask);
}
}
auto progressStep = std::make_shared<TaskStepProgress>();
connect(hashingTask.get(), &Task::finished, this, [this, progressStep] {
progressStep->state = TaskStepState::Succeeded;
stepProgress(*progressStep);
});
connect(hashingTask.get(), &Task::succeeded, this, &FlamePackExportTask::makeApiRequest);
connect(hashingTask.get(), &Task::failed, this, [this, progressStep](QString reason) {
progressStep->state = TaskStepState::Failed;
stepProgress(*progressStep);
emitFailed(reason);
});
connect(hashingTask.get(), &Task::stepProgress, this, &FlamePackExportTask::propagateStepProgress);
connect(hashingTask.get(), &Task::progress, this, [this, progressStep](qint64 current, qint64 total) {
progressStep->update(current, total);
stepProgress(*progressStep);
});
connect(hashingTask.get(), &Task::status, this, [this, progressStep](QString status) {
progressStep->status = status;
stepProgress(*progressStep);
});
hashingTask->start();
}
void FlamePackExportTask::makeApiRequest()
{
if (pendingHashes.isEmpty()) {
buildZip();
return;
}
setStatus(tr("Finding versions for hashes..."));
setProgress(2, 5);
auto response = std::make_shared<QByteArray>();
QList<uint> fingerprints;
for (auto& murmur : pendingHashes.keys()) {
fingerprints.push_back(murmur.toUInt());
}
task.reset(api.matchFingerprints(fingerprints, response));
connect(task.get(), &Task::succeeded, this, [this, response] {
QJsonParseError parseError{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parseError);
if (parseError.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from CurseForge::CurrentVersions at " << parseError.offset
<< " reason: " << parseError.errorString();
qWarning() << *response;
failed(parseError.errorString());
return;
}
try {
auto docObj = Json::requireObject(doc);
auto dataObj = Json::requireObject(docObj, "data");
auto dataArr = Json::requireArray(dataObj, "exactMatches");
if (dataArr.isEmpty()) {
qWarning() << "No matches found for fingerprint search!";
return;
}
for (auto match : dataArr) {
auto matchObj = Json::ensureObject(match, {});
auto fileObj = Json::ensureObject(matchObj, "file", {});
if (matchObj.isEmpty() || fileObj.isEmpty()) {
qWarning() << "Fingerprint match is empty!";
return;
}
auto fingerprint = QString::number(Json::ensureVariant(fileObj, "fileFingerprint").toUInt());
auto mod = pendingHashes.find(fingerprint);
if (mod == pendingHashes.end()) {
qWarning() << "Invalid fingerprint from the API response.";
continue;
}
setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(mod->name));
if (Json::ensureBoolean(fileObj, "isAvailable", false, "isAvailable"))
resolvedFiles.insert(mod->path, { Json::requireInteger(fileObj, "modId"), Json::requireInteger(fileObj, "id"),
mod->enabled, mod->isMod });
}
} catch (Json::JsonException& e) {
qDebug() << e.cause();
qDebug() << doc;
}
pendingHashes.clear();
});
connect(task.get(), &Task::finished, this, &FlamePackExportTask::getProjectsInfo);
connect(task.get(), &NetJob::failed, this, &FlamePackExportTask::emitFailed);
task->start();
}
void FlamePackExportTask::getProjectsInfo()
{
setStatus(tr("Finding project info from CurseForge..."));
setProgress(3, 5);
QStringList addonIds;
for (const auto& resolved : resolvedFiles) {
if (resolved.slug.isEmpty()) {
addonIds << QString::number(resolved.addonId);
}
}
auto response = std::make_shared<QByteArray>();
Task::Ptr projTask;
if (addonIds.isEmpty()) {
buildZip();
return;
} else if (addonIds.size() == 1) {
projTask = api.getProject(*addonIds.begin(), response);
} else {
projTask = api.getProjects(addonIds, response);
}
connect(projTask.get(), &Task::succeeded, this, [this, response, addonIds] {
QJsonParseError parseError{};
auto doc = QJsonDocument::fromJson(*response, &parseError);
if (parseError.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from CurseForge projects task at " << parseError.offset
<< " reason: " << parseError.errorString();
qWarning() << *response;
failed(parseError.errorString());
return;
}
try {
QJsonArray entries;
if (addonIds.size() == 1)
entries = { Json::requireObject(Json::requireObject(doc), "data") };
else
entries = Json::requireArray(Json::requireObject(doc), "data");
for (auto entry : entries) {
auto entryObj = Json::requireObject(entry);
try {
setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(Json::requireString(entryObj, "name")));
ModPlatform::IndexedPack pack;
FlameMod::loadIndexedPack(pack, entryObj);
for (auto key : resolvedFiles.keys()) {
auto val = resolvedFiles.value(key);
if (val.addonId == pack.addonId) {
val.name = pack.name;
val.slug = pack.slug;
QStringList authors;
for (auto author : pack.authors)
authors << author.name;
val.authors = authors.join(", ");
resolvedFiles[key] = val;
}
}
} catch (Json::JsonException& e) {
qDebug() << e.cause();
qDebug() << entries;
}
}
} catch (Json::JsonException& e) {
qDebug() << e.cause();
qDebug() << doc;
}
buildZip();
});
task.reset(projTask);
task->start();
}
void FlamePackExportTask::buildZip()
{
setStatus(tr("Adding files..."));
setProgress(4, 5);
buildZipFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this]() {
QuaZip zip(output);
if (!zip.open(QuaZip::mdCreate)) {
QFile::remove(output);
return BuildZipResult(tr("Could not create file"));
}
if (buildZipFuture.isCanceled())
return BuildZipResult();
QuaZipFile indexFile(&zip);
if (!indexFile.open(QIODevice::WriteOnly, QuaZipNewInfo("manifest.json"))) {
QFile::remove(output);
return BuildZipResult(tr("Could not create index"));
}
indexFile.write(generateIndex());
QuaZipFile modlist(&zip);
if (!modlist.open(QIODevice::WriteOnly, QuaZipNewInfo("modlist.html"))) {
QFile::remove(output);
return BuildZipResult(tr("Could not create index"));
}
QString content = "";
for (auto mod : resolvedFiles) {
if (mod.isMod) {
content += QString(TEMPLATE)
.replace("{name}", mod.name.toHtmlEscaped())
.replace("{url}", ModPlatform::getMetaURL(ModPlatform::ResourceProvider::FLAME, mod.addonId).toHtmlEscaped())
.replace("{authors}", !mod.authors.isEmpty() ? QString(" (by %1)").arg(mod.authors).toHtmlEscaped() : "");
}
}
content = "<ul>" + content + "</ul>";
modlist.write(content.toUtf8());
auto progressStep = std::make_shared<TaskStepProgress>();
size_t progress = 0;
for (const QFileInfo& file : files) {
if (buildZipFuture.isCanceled()) {
QFile::remove(output);
progressStep->state = TaskStepState::Failed;
stepProgress(*progressStep);
return BuildZipResult();
}
progressStep->update(progress, files.length());
stepProgress(*progressStep);
const QString relative = gameRoot.relativeFilePath(file.absoluteFilePath());
if (!resolvedFiles.contains(file.absoluteFilePath()) &&
!JlCompress::compressFile(&zip, file.absoluteFilePath(), "overrides/" + relative)) {
QFile::remove(output);
return BuildZipResult(tr("Could not read and compress %1").arg(relative));
}
progress++;
}
zip.close();
if (zip.getZipError() != 0) {
QFile::remove(output);
progressStep->state = TaskStepState::Failed;
stepProgress(*progressStep);
return BuildZipResult(tr("A zip error occurred"));
}
progressStep->state = TaskStepState::Succeeded;
stepProgress(*progressStep);
return BuildZipResult();
});
connect(&buildZipWatcher, &QFutureWatcher<BuildZipResult>::finished, this, &FlamePackExportTask::finish);
buildZipWatcher.setFuture(buildZipFuture);
}
void FlamePackExportTask::finish()
{
if (buildZipFuture.isCanceled())
emitAborted();
else {
const BuildZipResult result = buildZipFuture.result();
if (result.has_value())
emitFailed(result.value());
else
emitSucceeded();
}
}
QByteArray FlamePackExportTask::generateIndex()
{
QJsonObject obj;
obj["manifestType"] = "minecraftModpack";
obj["manifestVersion"] = 1;
obj["name"] = name;
obj["version"] = version;
obj["author"] = author;
obj["overrides"] = "overrides";
if (mcInstance) {
QJsonObject version;
auto profile = mcInstance->getPackProfile();
// collect all supported components
const ComponentPtr minecraft = profile->getComponent("net.minecraft");
const ComponentPtr quilt = profile->getComponent("org.quiltmc.quilt-loader");
const ComponentPtr fabric = profile->getComponent("net.fabricmc.fabric-loader");
const ComponentPtr forge = profile->getComponent("net.minecraftforge");
// convert all available components to mrpack dependencies
if (minecraft != nullptr)
version["version"] = minecraft->m_version;
QString id;
if (quilt != nullptr)
id = "quilt-" + quilt->getVersion();
else if (fabric != nullptr)
id = "fabric-" + fabric->getVersion();
else if (forge != nullptr)
id = "forge-" + forge->getVersion();
version["modLoaders"] = QJsonArray();
if (!id.isEmpty()) {
QJsonObject loader;
loader["id"] = id;
loader["primary"] = true;
version["modLoaders"] = QJsonArray({ loader });
}
obj["minecraft"] = version;
}
QJsonArray files;
for (auto mod : resolvedFiles) {
QJsonObject file;
file["projectID"] = mod.addonId;
file["fileID"] = mod.version;
file["required"] = mod.enabled;
files << file;
}
obj["files"] = files;
return QJsonDocument(obj).toJson(QJsonDocument::Compact);
}

View File

@ -0,0 +1,90 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QFuture>
#include <QFutureWatcher>
#include "BaseInstance.h"
#include "MMCZip.h"
#include "minecraft/MinecraftInstance.h"
#include "modplatform/flame/FlameAPI.h"
#include "tasks/Task.h"
class FlamePackExportTask : public Task {
public:
FlamePackExportTask(const QString& name,
const QString& version,
const QString& author,
InstancePtr instance,
const QString& output,
MMCZip::FilterFunction filter);
protected:
void executeTask() override;
bool abort() override;
private:
static const QString TEMPLATE;
static const QStringList FILE_EXTENSIONS;
// inputs
const QString name, version, author;
const InstancePtr instance;
MinecraftInstance* mcInstance;
const QDir gameRoot;
const QString output;
const MMCZip::FilterFunction filter;
typedef std::optional<QString> BuildZipResult;
struct ResolvedFile {
int addonId;
int version;
bool enabled;
bool isMod;
QString name;
QString slug;
QString authors;
};
struct HashInfo {
QString name;
QString path;
bool enabled;
bool isMod;
};
FlameAPI api;
QFileInfoList files;
QMap<QString, HashInfo> pendingHashes{};
QMap<QString, ResolvedFile> resolvedFiles{};
Task::Ptr task;
QFuture<BuildZipResult> buildZipFuture;
QFutureWatcher<BuildZipResult> buildZipWatcher;
void collectFiles();
void collectHashes();
void makeApiRequest();
void getProjectsInfo();
void buildZip();
void finish();
QByteArray generateIndex();
};

View File

@ -76,13 +76,8 @@ bool Flame::File::parseFromObject(const QJsonObject& obj, bool throw_on_blocked
// It is also optional // It is also optional
type = File::Type::SingleFile; type = File::Type::SingleFile;
if (fileName.endsWith(".zip")) {
// this is probably a resource pack
targetFolder = "resourcepacks";
} else {
// this is probably a mod, dunno what else could modpacks download
targetFolder = "mods"; targetFolder = "mods";
}
// get the hash // get the hash
hash = QString(); hash = QString();
auto hashes = Json::ensureArray(obj, "hashes"); auto hashes = Json::ensureArray(obj, "hashes");

View File

@ -0,0 +1,200 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "ExportToModList.h"
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
namespace ExportToModList {
QString toHTML(QList<Mod*> mods, OptionalData extraData)
{
QStringList lines;
for (auto mod : mods) {
auto meta = mod->metadata();
auto modName = mod->name().toHtmlEscaped();
if (extraData & Url) {
auto url = mod->metaurl().toHtmlEscaped();
if (!url.isEmpty())
modName = QString("<a href=\"%1\">%2</a>").arg(url, modName);
}
auto line = modName;
if (extraData & Version) {
auto ver = mod->version();
if (ver.isEmpty() && meta != nullptr)
ver = meta->version().toString();
if (!ver.isEmpty())
line += QString(" [%1]").arg(ver.toHtmlEscaped());
}
if (extraData & Authors && !mod->authors().isEmpty())
line += " by " + mod->authors().join(", ").toHtmlEscaped();
lines.append(QString("<li>%1</li>").arg(line));
}
return QString("<html><body><ul>\n\t%1\n</ul></body></html>").arg(lines.join("\n\t"));
}
QString toMarkdown(QList<Mod*> mods, OptionalData extraData)
{
QStringList lines;
for (auto mod : mods) {
auto meta = mod->metadata();
auto modName = mod->name();
if (extraData & Url) {
auto url = mod->metaurl();
if (!url.isEmpty())
modName = QString("[%1](%2)").arg(modName, url);
}
auto line = modName;
if (extraData & Version) {
auto ver = mod->version();
if (ver.isEmpty() && meta != nullptr)
ver = meta->version().toString();
if (!ver.isEmpty())
line += QString(" [%1]").arg(ver);
}
if (extraData & Authors && !mod->authors().isEmpty())
line += " by " + mod->authors().join(", ");
lines << "- " + line;
}
return lines.join("\n");
}
QString toPlainTXT(QList<Mod*> mods, OptionalData extraData)
{
QStringList lines;
for (auto mod : mods) {
auto meta = mod->metadata();
auto modName = mod->name();
auto line = modName;
if (extraData & Url) {
auto url = mod->metaurl();
if (!url.isEmpty())
line += QString(" (%1)").arg(url);
}
if (extraData & Version) {
auto ver = mod->version();
if (ver.isEmpty() && meta != nullptr)
ver = meta->version().toString();
if (!ver.isEmpty())
line += QString(" [%1]").arg(ver);
}
if (extraData & Authors && !mod->authors().isEmpty())
line += " by " + mod->authors().join(", ");
lines << line;
}
return lines.join("\n");
}
QString toJSON(QList<Mod*> mods, OptionalData extraData)
{
QJsonArray lines;
for (auto mod : mods) {
auto meta = mod->metadata();
auto modName = mod->name();
QJsonObject line;
line["name"] = modName;
if (extraData & Url) {
auto url = mod->metaurl();
if (!url.isEmpty())
line["url"] = url;
}
if (extraData & Version) {
auto ver = mod->version();
if (ver.isEmpty() && meta != nullptr)
ver = meta->version().toString();
if (!ver.isEmpty())
line["version"] = ver;
}
if (extraData & Authors && !mod->authors().isEmpty())
line["authors"] = QJsonArray::fromStringList(mod->authors());
lines << line;
}
QJsonDocument doc;
doc.setArray(lines);
return doc.toJson();
}
QString toCSV(QList<Mod*> mods, OptionalData extraData)
{
QStringList lines;
for (auto mod : mods) {
QStringList data;
auto meta = mod->metadata();
auto modName = mod->name();
data << modName;
if (extraData & Url)
data << mod->metaurl();
if (extraData & Version) {
auto ver = mod->version();
if (ver.isEmpty() && meta != nullptr)
ver = meta->version().toString();
data << ver;
}
if (extraData & Authors) {
QString authors;
if (mod->authors().length() == 1)
authors = mod->authors().back();
else if (mod->authors().length() > 1)
authors = QString("\"%1\"").arg(mod->authors().join(","));
data << authors;
}
lines << data.join(",");
}
return lines.join("\n");
}
QString exportToModList(QList<Mod*> mods, Formats format, OptionalData extraData)
{
switch (format) {
case HTML:
return toHTML(mods, extraData);
case MARKDOWN:
return toMarkdown(mods, extraData);
case PLAINTXT:
return toPlainTXT(mods, extraData);
case JSON:
return toJSON(mods, extraData);
case CSV:
return toCSV(mods, extraData);
default: {
return QString("unknown format:%1").arg(format);
}
}
}
QString exportToModList(QList<Mod*> mods, QString lineTemplate)
{
QStringList lines;
for (auto mod : mods) {
auto meta = mod->metadata();
auto modName = mod->name();
auto url = mod->metaurl();
auto ver = mod->version();
if (ver.isEmpty() && meta != nullptr)
ver = meta->version().toString();
auto authors = mod->authors().join(", ");
lines << QString(lineTemplate)
.replace("{name}", modName)
.replace("{url}", url)
.replace("{version}", ver)
.replace("{authors}", authors);
}
return lines.join("\n");
}
} // namespace ExportToModList

View File

@ -0,0 +1,33 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QList>
#include <QString>
#include "minecraft/mod/Mod.h"
namespace ExportToModList {
enum Formats { HTML, MARKDOWN, PLAINTXT, JSON, CSV, CUSTOM };
enum OptionalData {
Authors = 1 << 0,
Url = 1 << 1,
Version = 1 << 2,
};
QString exportToModList(QList<Mod*> mods, Formats format, OptionalData extraData);
QString exportToModList(QList<Mod*> mods, QString lineTemplate);
} // namespace ExportToModList

View File

@ -37,16 +37,16 @@
#include <QtConcurrent> #include <QtConcurrent>
#include "MMCZip.h"
#include "BaseInstance.h" #include "BaseInstance.h"
#include "FileSystem.h" #include "FileSystem.h"
#include "settings/INISettingsObject.h" #include "MMCZip.h"
#include "minecraft/GradleSpecifier.h"
#include "minecraft/MinecraftInstance.h" #include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h" #include "minecraft/PackProfile.h"
#include "minecraft/GradleSpecifier.h" #include "settings/INISettingsObject.h"
#include "BuildConfig.h"
#include "Application.h" #include "Application.h"
#include "BuildConfig.h"
namespace LegacyFTB { namespace LegacyFTB {
@ -65,6 +65,7 @@ void PackInstallTask::executeTask()
void PackInstallTask::downloadPack() void PackInstallTask::downloadPack()
{ {
setStatus(tr("Downloading zip for %1").arg(m_pack.name)); setStatus(tr("Downloading zip for %1").arg(m_pack.name));
setProgress(1, 4);
setAbortable(false); setAbortable(false);
archivePath = QString("%1/%2/%3").arg(m_pack.dir, m_version.replace(".", "_"), m_pack.file); archivePath = QString("%1/%2/%3").arg(m_pack.dir, m_version.replace(".", "_"), m_pack.file);
@ -78,11 +79,10 @@ void PackInstallTask::downloadPack()
} }
netJobContainer->addNetAction(Net::Download::makeFile(url, archivePath)); netJobContainer->addNetAction(Net::Download::makeFile(url, archivePath));
connect(netJobContainer.get(), &NetJob::succeeded, this, &PackInstallTask::onDownloadSucceeded); connect(netJobContainer.get(), &NetJob::succeeded, this, &PackInstallTask::unzip);
connect(netJobContainer.get(), &NetJob::failed, this, &PackInstallTask::onDownloadFailed); connect(netJobContainer.get(), &NetJob::failed, this, &PackInstallTask::emitFailed);
connect(netJobContainer.get(), &NetJob::progress, this, &PackInstallTask::onDownloadProgress); connect(netJobContainer.get(), &NetJob::stepProgress, this, &PackInstallTask::propagateStepProgress);
connect(netJobContainer.get(), &NetJob::stepProgress, this, &PackInstallTask::propogateStepProgress); connect(netJobContainer.get(), &NetJob::aborted, this, &PackInstallTask::emitAborted);
connect(netJobContainer.get(), &NetJob::aborted, this, &PackInstallTask::onDownloadAborted);
netJobContainer->start(); netJobContainer->start();
@ -90,27 +90,6 @@ void PackInstallTask::downloadPack()
progress(1, 4); progress(1, 4);
} }
void PackInstallTask::onDownloadSucceeded()
{
unzip();
}
void PackInstallTask::onDownloadFailed(QString reason)
{
emitFailed(reason);
}
void PackInstallTask::onDownloadProgress(qint64 current, qint64 total)
{
progress(current, total * 4);
setStatus(tr("Downloading zip for %1 (%2%)").arg(m_pack.name).arg(current / 10));
}
void PackInstallTask::onDownloadAborted()
{
emitAborted();
}
void PackInstallTask::unzip() void PackInstallTask::unzip()
{ {
setStatus(tr("Extracting modpack")); setStatus(tr("Extracting modpack"));
@ -120,16 +99,17 @@ void PackInstallTask::unzip()
QDir extractDir(m_stagingPath); QDir extractDir(m_stagingPath);
m_packZip.reset(new QuaZip(archivePath)); m_packZip.reset(new QuaZip(archivePath));
if(!m_packZip->open(QuaZip::mdUnzip)) if (!m_packZip->open(QuaZip::mdUnzip)) {
{
emitFailed(tr("Failed to open modpack file %1!").arg(archivePath)); emitFailed(tr("Failed to open modpack file %1!").arg(archivePath));
return; return;
} }
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), QOverload<QString, QString>::of(MMCZip::extractDir), archivePath, extractDir.absolutePath() + "/unzip"); m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), QOverload<QString, QString>::of(MMCZip::extractDir), archivePath,
extractDir.absolutePath() + "/unzip");
#else #else
m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractDir, archivePath, extractDir.absolutePath() + "/unzip"); m_extractFuture =
QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractDir, archivePath, extractDir.absolutePath() + "/unzip");
#endif #endif
connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, &PackInstallTask::onUnzipFinished); connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, &PackInstallTask::onUnzipFinished);
connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::canceled, this, &PackInstallTask::onUnzipCanceled); connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::canceled, this, &PackInstallTask::onUnzipCanceled);
@ -151,11 +131,9 @@ void PackInstallTask::install()
setStatus(tr("Installing modpack")); setStatus(tr("Installing modpack"));
progress(3, 4); progress(3, 4);
QDir unzipMcDir(m_stagingPath + "/unzip/minecraft"); QDir unzipMcDir(m_stagingPath + "/unzip/minecraft");
if(unzipMcDir.exists()) if (unzipMcDir.exists()) {
{
// ok, found minecraft dir, move contents to instance dir // ok, found minecraft dir, move contents to instance dir
if(!QDir().rename(m_stagingPath + "/unzip/minecraft", m_stagingPath + "/.minecraft")) if (!QDir().rename(m_stagingPath + "/unzip/minecraft", m_stagingPath + "/.minecraft")) {
{
emitFailed(tr("Failed to move unzipped Minecraft!")); emitFailed(tr("Failed to move unzipped Minecraft!"));
return; return;
} }
@ -175,8 +153,7 @@ void PackInstallTask::install()
// handle different versions // handle different versions
QFile packJson(m_stagingPath + "/.minecraft/pack.json"); QFile packJson(m_stagingPath + "/.minecraft/pack.json");
QDir jarmodDir = QDir(m_stagingPath + "/unzip/instMods"); QDir jarmodDir = QDir(m_stagingPath + "/unzip/instMods");
if(packJson.exists()) if (packJson.exists()) {
{
packJson.open(QIODevice::ReadOnly | QIODevice::Text); packJson.open(QIODevice::ReadOnly | QIODevice::Text);
QJsonDocument doc = QJsonDocument::fromJson(packJson.readAll()); QJsonDocument doc = QJsonDocument::fromJson(packJson.readAll());
packJson.close(); packJson.close();
@ -184,11 +161,9 @@ void PackInstallTask::install()
// we only care about the libs // we only care about the libs
QJsonArray libs = doc.object().value("libraries").toArray(); QJsonArray libs = doc.object().value("libraries").toArray();
foreach (const QJsonValue &value, libs) foreach (const QJsonValue& value, libs) {
{
QString nameValue = value.toObject().value("name").toString(); QString nameValue = value.toObject().value("name").toString();
if(!nameValue.startsWith("net.minecraftforge")) if (!nameValue.startsWith("net.minecraftforge")) {
{
continue; continue;
} }
@ -199,16 +174,13 @@ void PackInstallTask::install()
fallback = false; fallback = false;
break; break;
} }
} }
if(jarmodDir.exists()) if (jarmodDir.exists()) {
{
qDebug() << "Found jarmods, installing..."; qDebug() << "Found jarmods, installing...";
QStringList jarmods; QStringList jarmods;
for (auto info: jarmodDir.entryInfoList(QDir::NoDotAndDotDot | QDir::Files)) for (auto info : jarmodDir.entryInfoList(QDir::NoDotAndDotDot | QDir::Files)) {
{
qDebug() << "Jarmod:" << info.fileName(); qDebug() << "Jarmod:" << info.fileName();
jarmods.push_back(info.absoluteFilePath()); jarmods.push_back(info.absoluteFilePath());
} }
@ -220,8 +192,7 @@ void PackInstallTask::install()
// just nuke unzip directory, it s not needed anymore // just nuke unzip directory, it s not needed anymore
FS::deletePath(m_stagingPath + "/unzip"); FS::deletePath(m_stagingPath + "/unzip");
if(fallback) if (fallback) {
{
// TODO: Some fallback mechanism... or just keep failing! // TODO: Some fallback mechanism... or just keep failing!
emitFailed(tr("No installation method found!")); emitFailed(tr("No installation method found!"));
return; return;
@ -232,8 +203,7 @@ void PackInstallTask::install()
progress(4, 4); progress(4, 4);
instance.setName(name()); instance.setName(name());
if(m_instIcon == "default") if (m_instIcon == "default") {
{
m_instIcon = "ftb_logo"; m_instIcon = "ftb_logo";
} }
instance.setIconKey(m_instIcon); instance.setIconKey(m_instIcon);
@ -252,4 +222,4 @@ bool PackInstallTask::abort()
return InstanceTask::abort(); return InstanceTask::abort();
} }
} } // namespace LegacyFTB

View File

@ -1,12 +1,12 @@
#pragma once #pragma once
#include "InstanceTask.h"
#include "net/NetJob.h"
#include <quazip/quazip.h> #include <quazip/quazip.h>
#include <quazip/quazipdir.h> #include <quazip/quazipdir.h>
#include "InstanceTask.h"
#include "PackHelpers.h"
#include "meta/Index.h" #include "meta/Index.h"
#include "meta/Version.h" #include "meta/Version.h"
#include "meta/VersionList.h" #include "meta/VersionList.h"
#include "PackHelpers.h" #include "net/NetJob.h"
#include "net/NetJob.h" #include "net/NetJob.h"
@ -14,8 +14,7 @@
namespace LegacyFTB { namespace LegacyFTB {
class PackInstallTask : public InstanceTask class PackInstallTask : public InstanceTask {
{
Q_OBJECT Q_OBJECT
public: public:
@ -35,10 +34,6 @@ private:
void install(); void install();
private slots: private slots:
void onDownloadSucceeded();
void onDownloadFailed(QString reason);
void onDownloadProgress(qint64 current, qint64 total);
void onDownloadAborted();
void onUnzipFinished(); void onUnzipFinished();
void onUnzipCanceled(); void onUnzipCanceled();
@ -56,4 +51,4 @@ private: /* data */
QString m_version; QString m_version;
}; };
} } // namespace LegacyFTB

View File

@ -38,7 +38,7 @@ class ModrinthAPI : public NetworkResourceAPI {
static auto getModLoaderStrings(const ModLoaderTypes types) -> const QStringList static auto getModLoaderStrings(const ModLoaderTypes types) -> const QStringList
{ {
QStringList l; QStringList l;
for (auto loader : { Forge, Fabric, Quilt }) { for (auto loader : { Forge, Fabric, Quilt, LiteLoader }) {
if (types & loader) { if (types & loader) {
l << getModLoaderString(loader); l << getModLoaderString(loader);
} }
@ -92,7 +92,7 @@ class ModrinthAPI : public NetworkResourceAPI {
{ {
if (args.loaders.has_value()) { if (args.loaders.has_value()) {
if (!validateModLoaders(args.loaders.value())) { if (!validateModLoaders(args.loaders.value())) {
qWarning() << "Modrinth only have Forge and Fabric-compatible mods!"; qWarning() << "Modrinth - or our interface - does not support any the provided mod loaders!";
return {}; return {};
} }
} }
@ -141,7 +141,7 @@ class ModrinthAPI : public NetworkResourceAPI {
return s.isEmpty() ? QString() : s; return s.isEmpty() ? QString() : s;
} }
inline auto validateModLoaders(ModLoaderTypes loaders) const -> bool { return loaders & (Forge | Fabric | Quilt); } static inline auto validateModLoaders(ModLoaderTypes loaders) -> bool { return loaders & (Forge | Fabric | Quilt | LiteLoader); }
[[nodiscard]] std::optional<QString> getDependencyURL(DependencySearchArgs const& args) const override [[nodiscard]] std::optional<QString> getDependencyURL(DependencySearchArgs const& args) const override
{ {

View File

@ -267,7 +267,7 @@ bool ModrinthCreationTask::createInstance()
setDetails(tr("%1 out of %2 complete").arg(current).arg(total)); setDetails(tr("%1 out of %2 complete").arg(current).arg(total));
setProgress(current, total); setProgress(current, total);
}); });
connect(m_files_job.get(), &NetJob::stepProgress, this, &ModrinthCreationTask::propogateStepProgress); connect(m_files_job.get(), &NetJob::stepProgress, this, &ModrinthCreationTask::propagateStepProgress);
setStatus(tr("Downloading mods...")); setStatus(tr("Downloading mods..."));
m_files_job->start(); m_files_job->start();

View File

@ -64,7 +64,8 @@ bool ModrinthPackExportTask::abort()
if (buildZipFuture.isRunning()) { if (buildZipFuture.isRunning()) {
buildZipFuture.cancel(); buildZipFuture.cancel();
// NOTE: Here we don't do `emitAborted()` because it will be done when `buildZipFuture` actually cancels, which may not occur immediately. // NOTE: Here we don't do `emitAborted()` because it will be done when `buildZipFuture` actually cancels, which may not occur
// immediately.
return true; return true;
} }
@ -94,6 +95,7 @@ void ModrinthPackExportTask::collectFiles()
void ModrinthPackExportTask::collectHashes() void ModrinthPackExportTask::collectHashes()
{ {
setStatus(tr("Finding file hashes..."));
for (const QFileInfo& file : files) { for (const QFileInfo& file : files) {
QCoreApplication::processEvents(); QCoreApplication::processEvents();
@ -157,6 +159,7 @@ void ModrinthPackExportTask::makeApiRequest()
if (pendingHashes.isEmpty()) if (pendingHashes.isEmpty())
buildZip(); buildZip();
else { else {
setStatus(tr("Finding versions for hashes..."));
auto response = std::make_shared<QByteArray>(); auto response = std::make_shared<QByteArray>();
task = api.currentVersions(pendingHashes.values(), "sha512", response); task = api.currentVersions(pendingHashes.values(), "sha512", response);
connect(task.get(), &NetJob::succeeded, [this, response]() { parseApiResponse(response); }); connect(task.get(), &NetJob::succeeded, [this, response]() { parseApiResponse(response); });
@ -263,13 +266,13 @@ void ModrinthPackExportTask::finish()
QByteArray ModrinthPackExportTask::generateIndex() QByteArray ModrinthPackExportTask::generateIndex()
{ {
QJsonObject obj; QJsonObject out;
obj["formatVersion"] = 1; out["formatVersion"] = 1;
obj["game"] = "minecraft"; out["game"] = "minecraft";
obj["name"] = name; out["name"] = name;
obj["versionId"] = version; out["versionId"] = version;
if (!summary.isEmpty()) if (!summary.isEmpty())
obj["summary"] = summary; out["summary"] = summary;
if (mcInstance) { if (mcInstance) {
auto profile = mcInstance->getPackProfile(); auto profile = mcInstance->getPackProfile();
@ -290,30 +293,40 @@ QByteArray ModrinthPackExportTask::generateIndex()
if (forge != nullptr) if (forge != nullptr)
dependencies["forge"] = forge->m_version; dependencies["forge"] = forge->m_version;
obj["dependencies"] = dependencies; out["dependencies"] = dependencies;
} }
QJsonArray files; QJsonArray filesOut;
QMapIterator<QString, ResolvedFile> iterator(resolvedFiles); for (auto iterator = resolvedFiles.constBegin(); iterator != resolvedFiles.constEnd(); iterator++) {
while (iterator.hasNext()) { QJsonObject fileOut;
iterator.next();
QString path = iterator.key();
const ResolvedFile& value = iterator.value(); const ResolvedFile& value = iterator.value();
QJsonObject file; // detect disabled mod
file["path"] = iterator.key(); const QFileInfo pathInfo(path);
file["downloads"] = QJsonArray({ iterator.value().url }); if (pathInfo.suffix() == "disabled") {
// rename it
path = pathInfo.dir().filePath(pathInfo.completeBaseName());
// ...and make it optional
QJsonObject env;
env["client"] = "optional";
env["server"] = "optional";
fileOut["env"] = env;
}
fileOut["path"] = path;
fileOut["downloads"] = QJsonArray{ iterator.value().url };
QJsonObject hashes; QJsonObject hashes;
hashes["sha1"] = value.sha1; hashes["sha1"] = value.sha1;
hashes["sha512"] = value.sha512; hashes["sha512"] = value.sha512;
fileOut["hashes"] = hashes;
file["hashes"] = hashes; fileOut["fileSize"] = value.size;
file["fileSize"] = value.size; filesOut << fileOut;
files << file;
} }
obj["files"] = files; out["files"] = filesOut;
return QJsonDocument(obj).toJson(QJsonDocument::Compact); return QJsonDocument(out).toJson(QJsonDocument::Compact);
} }

View File

@ -50,7 +50,7 @@ void Technic::SingleZipPackInstallTask::executeTask()
auto job = m_filesNetJob.get(); auto job = m_filesNetJob.get();
connect(job, &NetJob::succeeded, this, &Technic::SingleZipPackInstallTask::downloadSucceeded); connect(job, &NetJob::succeeded, this, &Technic::SingleZipPackInstallTask::downloadSucceeded);
connect(job, &NetJob::progress, this, &Technic::SingleZipPackInstallTask::downloadProgressChanged); connect(job, &NetJob::progress, this, &Technic::SingleZipPackInstallTask::downloadProgressChanged);
connect(job, &NetJob::stepProgress, this, &Technic::SingleZipPackInstallTask::propogateStepProgress); connect(job, &NetJob::stepProgress, this, &Technic::SingleZipPackInstallTask::propagateStepProgress);
connect(job, &NetJob::failed, this, &Technic::SingleZipPackInstallTask::downloadFailed); connect(job, &NetJob::failed, this, &Technic::SingleZipPackInstallTask::downloadFailed);
m_filesNetJob->start(); m_filesNetJob->start();
} }

View File

@ -126,7 +126,7 @@ void Technic::SolderPackInstallTask::fileListSucceeded()
connect(m_filesNetJob.get(), &NetJob::succeeded, this, &Technic::SolderPackInstallTask::downloadSucceeded); connect(m_filesNetJob.get(), &NetJob::succeeded, this, &Technic::SolderPackInstallTask::downloadSucceeded);
connect(m_filesNetJob.get(), &NetJob::progress, this, &Technic::SolderPackInstallTask::downloadProgressChanged); connect(m_filesNetJob.get(), &NetJob::progress, this, &Technic::SolderPackInstallTask::downloadProgressChanged);
connect(m_filesNetJob.get(), &NetJob::stepProgress, this, &Technic::SolderPackInstallTask::propogateStepProgress); connect(m_filesNetJob.get(), &NetJob::stepProgress, this, &Technic::SolderPackInstallTask::propagateStepProgress);
connect(m_filesNetJob.get(), &NetJob::failed, this, &Technic::SolderPackInstallTask::downloadFailed); connect(m_filesNetJob.get(), &NetJob::failed, this, &Technic::SolderPackInstallTask::downloadFailed);
connect(m_filesNetJob.get(), &NetJob::aborted, this, &Technic::SolderPackInstallTask::downloadAborted); connect(m_filesNetJob.get(), &NetJob::aborted, this, &Technic::SolderPackInstallTask::downloadAborted);
m_filesNetJob->start(); m_filesNetJob->start();

View File

@ -161,7 +161,7 @@ void Task::emitSucceeded()
emit finished(); emit finished();
} }
void Task::propogateStepProgress(TaskStepProgress const& task_progress) void Task::propagateStepProgress(TaskStepProgress const& task_progress)
{ {
emit stepProgress(task_progress); emit stepProgress(task_progress);
} }

View File

@ -167,7 +167,7 @@ class Task : public QObject, public QRunnable {
virtual void emitAborted(); virtual void emitAborted();
virtual void emitFailed(QString reason = ""); virtual void emitFailed(QString reason = "");
virtual void propogateStepProgress(TaskStepProgress const& task_progress); virtual void propagateStepProgress(TaskStepProgress const& task_progress);
public slots: public slots:
void setStatus(const QString& status); void setStatus(const QString& status);

View File

@ -42,6 +42,7 @@
#include <QDir> #include <QDir>
#include <QLibraryInfo> #include <QLibraryInfo>
#include <QDebug> #include <QDebug>
#include <locale>
#include "FileSystem.h" #include "FileSystem.h"
#include "net/NetJob.h" #include "net/NetJob.h"
@ -454,6 +455,7 @@ QVariant TranslationsModel::data(const QModelIndex& index, int role) const
return QString("%1%").arg(lang.percentTranslated(), 3, 'f', 1); return QString("%1%").arg(lang.percentTranslated(), 3, 'f', 1);
} }
} }
qWarning("TranslationModel::data not implemented when role is DisplayRole");
} }
case Qt::ToolTipRole: case Qt::ToolTipRole:
{ {
@ -526,34 +528,34 @@ Language * TranslationsModel::findLanguage(const QString& key)
} }
} }
void TranslationsModel::setUseSystemLocale(bool useSystemLocale)
{
APPLICATION->settings()->set("UseSystemLocale", useSystemLocale);
QLocale::setDefault(QLocale(useSystemLocale ? QString::fromStdString(std::locale().name()) : defaultLangCode));
}
bool TranslationsModel::selectLanguage(QString key) bool TranslationsModel::selectLanguage(QString key)
{ {
QString& langCode = key; QString& langCode = key;
auto langPtr = findLanguage(key); auto langPtr = findLanguage(key);
if (langCode.isEmpty()) if (langCode.isEmpty()) {
{
d->no_language_set = true; d->no_language_set = true;
} }
if(!langPtr) if (!langPtr) {
{
qWarning() << "Selected invalid language" << key << ", defaulting to" << defaultLangCode; qWarning() << "Selected invalid language" << key << ", defaulting to" << defaultLangCode;
langCode = defaultLangCode; langCode = defaultLangCode;
} } else {
else
{
langCode = langPtr->key; langCode = langPtr->key;
} }
// uninstall existing translators if there are any // uninstall existing translators if there are any
if (d->m_app_translator) if (d->m_app_translator) {
{
QCoreApplication::removeTranslator(d->m_app_translator.get()); QCoreApplication::removeTranslator(d->m_app_translator.get());
d->m_app_translator.reset(); d->m_app_translator.reset();
} }
if (d->m_qt_translator) if (d->m_qt_translator) {
{
QCoreApplication::removeTranslator(d->m_qt_translator.get()); QCoreApplication::removeTranslator(d->m_qt_translator.get());
d->m_qt_translator.reset(); d->m_qt_translator.reset();
} }
@ -563,8 +565,9 @@ bool TranslationsModel::selectLanguage(QString key)
* In a multithreaded application, the default locale should be set at application startup, before any non-GUI threads are created. * In a multithreaded application, the default locale should be set at application startup, before any non-GUI threads are created.
* This function is not reentrant. * This function is not reentrant.
*/ */
QLocale locale = QLocale(langCode); QLocale::setDefault(
QLocale::setDefault(locale); QLocale(APPLICATION->settings()->get("UseSystemLocale").toBool() ? QString::fromStdString(std::locale().name()) : langCode));
// if it's the default UI language, finish // if it's the default UI language, finish
if(langCode == defaultLangCode) if(langCode == defaultLangCode)

View File

@ -20,8 +20,7 @@
struct Language; struct Language;
class TranslationsModel : public QAbstractListModel class TranslationsModel : public QAbstractListModel {
{
Q_OBJECT Q_OBJECT
public: public:
explicit TranslationsModel(QString path, QObject* parent = 0); explicit TranslationsModel(QString path, QObject* parent = 0);
@ -38,6 +37,7 @@ public:
QString selectedLanguage(); QString selectedLanguage();
void downloadIndex(); void downloadIndex();
void setUseSystemLocale(bool useSystemLocale);
private: private:
Language* findLanguage(const QString& key); Language* findLanguage(const QString& key);
@ -57,7 +57,6 @@ private slots:
void dlGood(); void dlGood();
void translationDirChanged(const QString& path); void translationDirChanged(const QString& path);
private: /* data */ private: /* data */
struct Private; struct Private;
std::unique_ptr<Private> d; std::unique_ptr<Private> d;

View File

@ -43,100 +43,98 @@
#include "FileSystem.h" #include "FileSystem.h"
#include "MainWindow.h" #include "MainWindow.h"
#include "ui/dialogs/ExportToModListDialog.h"
#include "ui_MainWindow.h" #include "ui_MainWindow.h"
#include <QVariant>
#include <QUrl>
#include <QDir> #include <QDir>
#include <QFileInfo> #include <QFileInfo>
#include <QUrl>
#include <QVariant>
#include <QKeyEvent>
#include <QAction> #include <QAction>
#include <QActionGroup> #include <QActionGroup>
#include <QApplication> #include <QApplication>
#include <QButtonGroup> #include <QButtonGroup>
#include <QFileDialog>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QHeaderView> #include <QHeaderView>
#include <QInputDialog>
#include <QKeyEvent>
#include <QLabel>
#include <QMainWindow> #include <QMainWindow>
#include <QStatusBar>
#include <QToolBar>
#include <QWidget>
#include <QMenu> #include <QMenu>
#include <QMenuBar> #include <QMenuBar>
#include <QMessageBox> #include <QMessageBox>
#include <QFileDialog>
#include <QInputDialog>
#include <QLabel>
#include <QToolButton>
#include <QWidgetAction>
#include <QProgressDialog> #include <QProgressDialog>
#include <QShortcut> #include <QShortcut>
#include <QStatusBar>
#include <QToolBar>
#include <QToolButton>
#include <QWidget>
#include <QWidgetAction>
#include <BaseInstance.h> #include <BaseInstance.h>
#include <InstanceList.h>
#include <minecraft/MinecraftInstance.h>
#include <MMCZip.h>
#include <icons/IconList.h>
#include <java/JavaUtils.h>
#include <java/JavaInstallList.h>
#include <launch/LaunchTask.h>
#include <minecraft/auth/AccountList.h>
#include <SkinUtils.h>
#include <BuildConfig.h> #include <BuildConfig.h>
#include <net/NetJob.h> #include <DesktopServices.h>
#include <InstanceList.h>
#include <MMCZip.h>
#include <SkinUtils.h>
#include <icons/IconList.h>
#include <java/JavaInstallList.h>
#include <java/JavaUtils.h>
#include <launch/LaunchTask.h>
#include <minecraft/MinecraftInstance.h>
#include <minecraft/auth/AccountList.h>
#include <net/Download.h> #include <net/Download.h>
#include <net/NetJob.h>
#include <news/NewsChecker.h> #include <news/NewsChecker.h>
#include <tools/BaseProfiler.h> #include <tools/BaseProfiler.h>
#include <updater/ExternalUpdater.h> #include <updater/ExternalUpdater.h>
#include <DesktopServices.h>
#include "InstanceWindow.h"
#include "InstancePageProvider.h" #include "InstancePageProvider.h"
#include "InstanceWindow.h"
#include "JavaCommon.h" #include "JavaCommon.h"
#include "LaunchController.h" #include "LaunchController.h"
#include "ui/instanceview/InstanceProxyModel.h" #include "ui/dialogs/AboutDialog.h"
#include "ui/instanceview/InstanceView.h" #include "ui/dialogs/CopyInstanceDialog.h"
#include "ui/instanceview/InstanceDelegate.h" #include "ui/dialogs/CustomMessageBox.h"
#include "ui/widgets/LabeledToolButton.h" #include "ui/dialogs/EditAccountDialog.h"
#include "ui/dialogs/ExportInstanceDialog.h"
#include "ui/dialogs/ExportPackDialog.h"
#include "ui/dialogs/IconPickerDialog.h"
#include "ui/dialogs/ImportResourceDialog.h"
#include "ui/dialogs/NewInstanceDialog.h" #include "ui/dialogs/NewInstanceDialog.h"
#include "ui/dialogs/NewsDialog.h" #include "ui/dialogs/NewsDialog.h"
#include "ui/dialogs/ProgressDialog.h" #include "ui/dialogs/ProgressDialog.h"
#include "ui/dialogs/AboutDialog.h" #include "ui/instanceview/InstanceDelegate.h"
#include "ui/dialogs/CustomMessageBox.h" #include "ui/instanceview/InstanceProxyModel.h"
#include "ui/dialogs/IconPickerDialog.h" #include "ui/instanceview/InstanceView.h"
#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/ITheme.h"
#include "ui/themes/ThemeManager.h" #include "ui/themes/ThemeManager.h"
#include "ui/widgets/LabeledToolButton.h"
#include "minecraft/mod/tasks/LocalResourceParse.h" #include "minecraft/WorldList.h"
#include "minecraft/mod/ModFolderModel.h" #include "minecraft/mod/ModFolderModel.h"
#include "minecraft/mod/ShaderPackFolderModel.h" #include "minecraft/mod/ShaderPackFolderModel.h"
#include "minecraft/WorldList.h" #include "minecraft/mod/tasks/LocalResourceParse.h"
#include "KonamiCode.h" #include "KonamiCode.h"
#include "InstanceImportTask.h"
#include "InstanceCopyTask.h" #include "InstanceCopyTask.h"
#include "InstanceImportTask.h"
#include "MMCTime.h" #include "MMCTime.h"
namespace { namespace {
QString profileInUseFilter(const QString& profile, bool used) QString profileInUseFilter(const QString& profile, bool used)
{ {
if(used) if (used) {
{
return QObject::tr("%1 (in use)").arg(profile); return QObject::tr("%1 (in use)").arg(profile);
} } else {
else
{
return profile; return profile;
} }
} }
} } // namespace
MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWindow) MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWindow)
{ {
@ -184,7 +182,6 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi
ui->instanceToolBar->addContextMenuAction(ui->newsToolBar->toggleViewAction()); ui->instanceToolBar->addContextMenuAction(ui->newsToolBar->toggleViewAction());
ui->instanceToolBar->addContextMenuAction(ui->instanceToolBar->toggleViewAction()); ui->instanceToolBar->addContextMenuAction(ui->instanceToolBar->toggleViewAction());
ui->instanceToolBar->addContextMenuAction(ui->actionLockToolbars); ui->instanceToolBar->addContextMenuAction(ui->actionLockToolbars);
} }
// set the menu for the folders help, accounts, and export tool buttons // set the menu for the folders help, accounts, and export tool buttons
@ -205,6 +202,8 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi
auto exportInstanceMenu = new QMenu(this); auto exportInstanceMenu = new QMenu(this);
exportInstanceMenu->addAction(ui->actionExportInstanceZip); exportInstanceMenu->addAction(ui->actionExportInstanceZip);
exportInstanceMenu->addAction(ui->actionExportInstanceMrPack); exportInstanceMenu->addAction(ui->actionExportInstanceMrPack);
exportInstanceMenu->addAction(ui->actionExportInstanceFlamePack);
exportInstanceMenu->addAction(ui->actionExportInstanceToModList);
ui->actionExportInstance->setMenu(exportInstanceMenu); ui->actionExportInstance->setMenu(exportInstanceMenu);
} }
@ -230,7 +229,6 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi
if (qgetenv("XDG_CURRENT_DESKTOP") == "gamescope") { if (qgetenv("XDG_CURRENT_DESKTOP") == "gamescope") {
ui->mainToolBar->addAction(ui->actionCloseWindow); ui->mainToolBar->addAction(ui->actionCloseWindow);
} }
} }
// add the toolbar toggles to the view menu // add the toolbar toggles to the view menu
@ -300,9 +298,8 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi
connect(proxymodel, &InstanceProxyModel::dataChanged, this, &MainWindow::instanceDataChanged); connect(proxymodel, &InstanceProxyModel::dataChanged, this, &MainWindow::instanceDataChanged);
view->setModel(proxymodel); view->setModel(proxymodel);
view->setSourceOfGroupCollapseStatus([](const QString & groupName)->bool { view->setSourceOfGroupCollapseStatus(
return APPLICATION->instances()->isGroupCollapsed(groupName); [](const QString& groupName) -> bool { return APPLICATION->instances()->isGroupCollapsed(groupName); });
});
connect(view, &InstanceView::groupStateChanged, APPLICATION->instances().get(), &InstanceList::on_GroupStateChanged); connect(view, &InstanceView::groupStateChanged, APPLICATION->instances().get(), &InstanceList::on_GroupStateChanged);
ui->horizontalLayout->addWidget(view); ui->horizontalLayout->addWidget(view);
} }
@ -360,21 +357,8 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi
// Update the menu when the active account changes. // Update the menu when the active account changes.
// Shouldn't have to use lambdas here like this, but if I don't, the compiler throws a fit. // Shouldn't have to use lambdas here like this, but if I don't, the compiler throws a fit.
// Template hell sucks... // Template hell sucks...
connect( connect(APPLICATION->accounts().get(), &AccountList::defaultAccountChanged, [this] { defaultAccountChanged(); });
APPLICATION->accounts().get(), connect(APPLICATION->accounts().get(), &AccountList::listChanged, [this] { repopulateAccountsMenu(); });
&AccountList::defaultAccountChanged,
[this] {
defaultAccountChanged();
}
);
connect(
APPLICATION->accounts().get(),
&AccountList::listChanged,
[this]
{
repopulateAccountsMenu();
}
);
// Show initial account // Show initial account
defaultAccountChanged(); defaultAccountChanged();
@ -426,7 +410,6 @@ void MainWindow::keyReleaseEvent(QKeyEvent *event)
void MainWindow::retranslateUi() void MainWindow::retranslateUi()
{ {
if (m_selectedInstance) { if (m_selectedInstance) {
m_statusLeft->setText(m_selectedInstance->getStatusbarDescription()); m_statusLeft->setText(m_selectedInstance->getStatusbarDescription());
} else { } else {
@ -456,9 +439,7 @@ void MainWindow::retranslateUi()
} }
} }
MainWindow::~MainWindow() MainWindow::~MainWindow() {}
{
}
QMenu* MainWindow::createPopupMenu() QMenu* MainWindow::createPopupMenu()
{ {
@ -477,10 +458,11 @@ void MainWindow::lockToolbars(bool state)
APPLICATION->settings()->set("ToolbarsLocked", state); APPLICATION->settings()->set("ToolbarsLocked", state);
} }
void MainWindow::konamiTriggered() void MainWindow::konamiTriggered()
{ {
QString gradient = " stop:0 rgba(125, 0, 0, 255), stop:0.166 rgba(125, 125, 0, 255), stop:0.333 rgba(0, 125, 0, 255), stop:0.5 rgba(0, 125, 125, 255), stop:0.666 rgba(0, 0, 125, 255), stop:0.833 rgba(125, 0, 125, 255), stop:1 rgba(125, 0, 0, 255));"; QString gradient =
" stop:0 rgba(125, 0, 0, 255), stop:0.166 rgba(125, 125, 0, 255), stop:0.333 rgba(0, 125, 0, 255), stop:0.5 rgba(0, 125, 125, "
"255), stop:0.666 rgba(0, 0, 125, 255), stop:0.833 rgba(125, 0, 125, 255), stop:1 rgba(125, 0, 0, 255));";
QString stylesheet = "background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:0," + gradient; QString stylesheet = "background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:0," + gradient;
if (ui->mainToolBar->styleSheet() == stylesheet) { if (ui->mainToolBar->styleSheet() == stylesheet) {
ui->mainToolBar->setStyleSheet(""); ui->mainToolBar->setStyleSheet("");
@ -507,8 +489,7 @@ void MainWindow::showInstanceContextMenu(const QPoint &pos)
actionSep->setSeparator(true); actionSep->setSeparator(true);
bool onInstance = view->indexAt(pos).isValid(); bool onInstance = view->indexAt(pos).isValid();
if (onInstance) if (onInstance) {
{
// reuse the file menu actions // reuse the file menu actions
actions = ui->fileMenu->actions(); actions = ui->fileMenu->actions();
@ -525,9 +506,7 @@ void MainWindow::showInstanceContextMenu(const QPoint &pos)
QAction* actionVoid = new QAction(m_selectedInstance->name(), this); QAction* actionVoid = new QAction(m_selectedInstance->name(), this);
actionVoid->setEnabled(false); actionVoid->setEnabled(false);
actions.prepend(actionVoid); actions.prepend(actionVoid);
} } else {
else
{
auto group = view->groupNameAt(pos); auto group = view->groupNameAt(pos);
QAction* actionVoid = new QAction(BuildConfig.LAUNCHER_DISPLAYNAME, this); QAction* actionVoid = new QAction(BuildConfig.LAUNCHER_DISPLAYNAME, this);
@ -535,8 +514,7 @@ void MainWindow::showInstanceContextMenu(const QPoint &pos)
QAction* actionCreateInstance = new QAction(tr("Create instance"), this); QAction* actionCreateInstance = new QAction(tr("Create instance"), this);
actionCreateInstance->setToolTip(ui->actionAddInstance->toolTip()); actionCreateInstance->setToolTip(ui->actionAddInstance->toolTip());
if(!group.isNull()) if (!group.isNull()) {
{
QVariantMap data; QVariantMap data;
data["group"] = group; data["group"] = group;
actionCreateInstance->setData(data); actionCreateInstance->setData(data);
@ -547,8 +525,7 @@ void MainWindow::showInstanceContextMenu(const QPoint &pos)
actions.prepend(actionSep); actions.prepend(actionSep);
actions.prepend(actionVoid); actions.prepend(actionVoid);
actions.append(actionCreateInstance); actions.append(actionCreateInstance);
if(!group.isNull()) if (!group.isNull()) {
{
QAction* actionDeleteGroup = new QAction(tr("Delete group '%1'").arg(group), this); QAction* actionDeleteGroup = new QAction(tr("Delete group '%1'").arg(group), this);
QVariantMap data; QVariantMap data;
data["group"] = group; data["group"] = group;
@ -581,12 +558,9 @@ void MainWindow::updateToolsMenu()
ui->actionLaunchInstanceDemo->setDisabled(!m_selectedInstance || currentInstanceRunning); ui->actionLaunchInstanceDemo->setDisabled(!m_selectedInstance || currentInstanceRunning);
QMenu* launchMenu = ui->actionLaunchInstance->menu(); QMenu* launchMenu = ui->actionLaunchInstance->menu();
if (launchMenu) if (launchMenu) {
{
launchMenu->clear(); launchMenu->clear();
} } else {
else
{
launchMenu = new QMenu(this); launchMenu = new QMenu(this);
} }
QAction* normalLaunch = launchMenu->addAction(tr("Launch")); QAction* normalLaunch = launchMenu->addAction(tr("Launch"));
@ -595,24 +569,15 @@ void MainWindow::updateToolsMenu()
normalLaunchOffline->setShortcut(QKeySequence(tr("Ctrl+Shift+O"))); normalLaunchOffline->setShortcut(QKeySequence(tr("Ctrl+Shift+O")));
QAction* normalLaunchDemo = launchMenu->addAction(tr("Launch Demo")); QAction* normalLaunchDemo = launchMenu->addAction(tr("Launch Demo"));
normalLaunchDemo->setShortcut(QKeySequence(tr("Ctrl+Alt+O"))); normalLaunchDemo->setShortcut(QKeySequence(tr("Ctrl+Alt+O")));
if (m_selectedInstance) if (m_selectedInstance) {
{
normalLaunch->setEnabled(m_selectedInstance->canLaunch()); normalLaunch->setEnabled(m_selectedInstance->canLaunch());
normalLaunchOffline->setEnabled(m_selectedInstance->canLaunch()); normalLaunchOffline->setEnabled(m_selectedInstance->canLaunch());
normalLaunchDemo->setEnabled(m_selectedInstance->canLaunch()); normalLaunchDemo->setEnabled(m_selectedInstance->canLaunch());
connect(normalLaunch, &QAction::triggered, [this]() { connect(normalLaunch, &QAction::triggered, [this]() { APPLICATION->launch(m_selectedInstance, true, false); });
APPLICATION->launch(m_selectedInstance, true, false); connect(normalLaunchOffline, &QAction::triggered, [this]() { APPLICATION->launch(m_selectedInstance, false, false); });
}); connect(normalLaunchDemo, &QAction::triggered, [this]() { APPLICATION->launch(m_selectedInstance, false, true); });
connect(normalLaunchOffline, &QAction::triggered, [this]() { } else {
APPLICATION->launch(m_selectedInstance, false, false);
});
connect(normalLaunchDemo, &QAction::triggered, [this]() {
APPLICATION->launch(m_selectedInstance, false, true);
});
}
else
{
normalLaunch->setDisabled(true); normalLaunch->setDisabled(true);
normalLaunchOffline->setDisabled(true); normalLaunchOffline->setDisabled(true);
normalLaunchDemo->setDisabled(true); normalLaunchDemo->setDisabled(true);
@ -626,35 +591,25 @@ void MainWindow::updateToolsMenu()
QString profilersTitle = tr("Profilers"); QString profilersTitle = tr("Profilers");
launchMenu->addSeparator()->setText(profilersTitle); launchMenu->addSeparator()->setText(profilersTitle);
for (auto profiler : APPLICATION->profilers().values()) for (auto profiler : APPLICATION->profilers().values()) {
{
QAction* profilerAction = launchMenu->addAction(profiler->name()); QAction* profilerAction = launchMenu->addAction(profiler->name());
QAction* profilerOfflineAction = launchMenu->addAction(tr("%1 Offline").arg(profiler->name())); QAction* profilerOfflineAction = launchMenu->addAction(tr("%1 Offline").arg(profiler->name()));
QString error; QString error;
if (!profiler->check(&error)) if (!profiler->check(&error)) {
{
profilerAction->setDisabled(true); profilerAction->setDisabled(true);
profilerOfflineAction->setDisabled(true); profilerOfflineAction->setDisabled(true);
QString profilerToolTip = tr("Profiler not setup correctly. Go into settings, \"External Tools\"."); QString profilerToolTip = tr("Profiler not setup correctly. Go into settings, \"External Tools\".");
profilerAction->setToolTip(profilerToolTip); profilerAction->setToolTip(profilerToolTip);
profilerOfflineAction->setToolTip(profilerToolTip); profilerOfflineAction->setToolTip(profilerToolTip);
} } else if (m_selectedInstance) {
else if (m_selectedInstance)
{
profilerAction->setEnabled(m_selectedInstance->canLaunch()); profilerAction->setEnabled(m_selectedInstance->canLaunch());
profilerOfflineAction->setEnabled(m_selectedInstance->canLaunch()); profilerOfflineAction->setEnabled(m_selectedInstance->canLaunch());
connect(profilerAction, &QAction::triggered, [this, profiler]() connect(profilerAction, &QAction::triggered,
{ [this, profiler]() { APPLICATION->launch(m_selectedInstance, true, false, profiler.get()); });
APPLICATION->launch(m_selectedInstance, true, false, profiler.get()); connect(profilerOfflineAction, &QAction::triggered,
}); [this, profiler]() { APPLICATION->launch(m_selectedInstance, false, false, profiler.get()); });
connect(profilerOfflineAction, &QAction::triggered, [this, profiler]() } else {
{
APPLICATION->launch(m_selectedInstance, false, false, profiler.get());
});
}
else
{
profilerAction->setDisabled(true); profilerAction->setDisabled(true);
profilerOfflineAction->setDisabled(true); profilerOfflineAction->setDisabled(true);
} }
@ -711,11 +666,9 @@ void MainWindow::repopulateAccountsMenu()
MinecraftAccountPtr defaultAccount = accounts->defaultAccount(); MinecraftAccountPtr defaultAccount = accounts->defaultAccount();
QString active_profileId = ""; QString active_profileId = "";
if (defaultAccount) if (defaultAccount) {
{
// this can be called before accountMenuButton exists // this can be called before accountMenuButton exists
if (ui->actionAccountsButton) if (ui->actionAccountsButton) {
{
auto profileLabel = profileInUseFilter(defaultAccount->profileName(), defaultAccount->isInUse()); auto profileLabel = profileInUseFilter(defaultAccount->profileName(), defaultAccount->isInUse());
ui->actionAccountsButton->setText(profileLabel); ui->actionAccountsButton->setText(profileLabel);
} }
@ -723,38 +676,31 @@ void MainWindow::repopulateAccountsMenu()
QActionGroup* accountsGroup = new QActionGroup(this); QActionGroup* accountsGroup = new QActionGroup(this);
if (accounts->count() <= 0) if (accounts->count() <= 0) {
{
ui->actionNoAccountsAdded->setEnabled(false); ui->actionNoAccountsAdded->setEnabled(false);
ui->accountsMenu->addAction(ui->actionNoAccountsAdded); ui->accountsMenu->addAction(ui->actionNoAccountsAdded);
} } else {
else
{
// TODO: Nicer way to iterate? // TODO: Nicer way to iterate?
for (int i = 0; i < accounts->count(); i++) for (int i = 0; i < accounts->count(); i++) {
{
MinecraftAccountPtr account = accounts->at(i); MinecraftAccountPtr account = accounts->at(i);
auto profileLabel = profileInUseFilter(account->profileName(), account->isInUse()); auto profileLabel = profileInUseFilter(account->profileName(), account->isInUse());
QAction* action = new QAction(profileLabel, this); QAction* action = new QAction(profileLabel, this);
action->setData(i); action->setData(i);
action->setCheckable(true); action->setCheckable(true);
action->setActionGroup(accountsGroup); action->setActionGroup(accountsGroup);
if (defaultAccount == account) if (defaultAccount == account) {
{
action->setChecked(true); action->setChecked(true);
} }
auto face = account->getFace(); auto face = account->getFace();
if (!face.isNull()) { if (!face.isNull()) {
action->setIcon(face); action->setIcon(face);
} } else {
else {
action->setIcon(APPLICATION->getThemedIcon("noaccount")); action->setIcon(APPLICATION->getThemedIcon("noaccount"));
} }
const int highestNumberKey = 9; const int highestNumberKey = 9;
if(i<highestNumberKey) if (i < highestNumberKey) {
{
action->setShortcut(QKeySequence(tr("Ctrl+%1").arg(i + 1))); action->setShortcut(QKeySequence(tr("Ctrl+%1").arg(i + 1)));
} }
@ -781,8 +727,7 @@ void MainWindow::repopulateAccountsMenu()
void MainWindow::updatesAllowedChanged(bool allowed) void MainWindow::updatesAllowedChanged(bool allowed)
{ {
if(!BuildConfig.UPDATER_ENABLED) if (!BuildConfig.UPDATER_ENABLED) {
{
return; return;
} }
ui->actionCheckUpdate->setEnabled(allowed); ui->actionCheckUpdate->setEnabled(allowed);
@ -817,15 +762,13 @@ void MainWindow::defaultAccountChanged()
MinecraftAccountPtr account = APPLICATION->accounts()->defaultAccount(); MinecraftAccountPtr account = APPLICATION->accounts()->defaultAccount();
// FIXME: this needs adjustment for MSA // FIXME: this needs adjustment for MSA
if (account && account->profileName() != "") if (account && account->profileName() != "") {
{
auto profileLabel = profileInUseFilter(account->profileName(), account->isInUse()); auto profileLabel = profileInUseFilter(account->profileName(), account->isInUse());
ui->actionAccountsButton->setText(profileLabel); ui->actionAccountsButton->setText(profileLabel);
auto face = account->getFace(); auto face = account->getFace();
if (face.isNull()) { if (face.isNull()) {
ui->actionAccountsButton->setIcon(APPLICATION->getThemedIcon("noaccount")); ui->actionAccountsButton->setIcon(APPLICATION->getThemedIcon("noaccount"));
} } else {
else {
ui->actionAccountsButton->setIcon(face); ui->actionAccountsButton->setIcon(face);
} }
return; return;
@ -838,14 +781,11 @@ void MainWindow::defaultAccountChanged()
bool MainWindow::eventFilter(QObject* obj, QEvent* ev) bool MainWindow::eventFilter(QObject* obj, QEvent* ev)
{ {
if (obj == view) if (obj == view) {
{ if (ev->type() == QEvent::KeyPress) {
if (ev->type() == QEvent::KeyPress)
{
secretEventFilter->input(ev); secretEventFilter->input(ev);
QKeyEvent* keyEvent = static_cast<QKeyEvent*>(ev); QKeyEvent* keyEvent = static_cast<QKeyEvent*>(ev);
switch (keyEvent->key()) switch (keyEvent->key()) {
{
/* /*
case Qt::Key_Enter: case Qt::Key_Enter:
case Qt::Key_Return: case Qt::Key_Return:
@ -871,23 +811,17 @@ bool MainWindow::eventFilter(QObject *obj, QEvent *ev)
void MainWindow::updateNewsLabel() void MainWindow::updateNewsLabel()
{ {
if (m_newsChecker->isLoadingNews()) if (m_newsChecker->isLoadingNews()) {
{
newsLabel->setText(tr("Loading news...")); newsLabel->setText(tr("Loading news..."));
newsLabel->setEnabled(false); newsLabel->setEnabled(false);
ui->actionMoreNews->setVisible(false); ui->actionMoreNews->setVisible(false);
} } else {
else
{
QList<NewsEntryPtr> entries = m_newsChecker->getNewsEntries(); QList<NewsEntryPtr> entries = m_newsChecker->getNewsEntries();
if (entries.length() > 0) if (entries.length() > 0) {
{
newsLabel->setText(entries[0]->title); newsLabel->setText(entries[0]->title);
newsLabel->setEnabled(true); newsLabel->setEnabled(true);
ui->actionMoreNews->setVisible(true); ui->actionMoreNews->setVisible(true);
} } else {
else
{
newsLabel->setText(tr("No news available.")); newsLabel->setText(tr("No news available."));
newsLabel->setEnabled(false); newsLabel->setEnabled(false);
ui->actionMoreNews->setVisible(false); ui->actionMoreNews->setVisible(false);
@ -903,8 +837,7 @@ QList<int> stringToIntList(const QString &string)
QStringList split = string.split(',', QString::SkipEmptyParts); QStringList split = string.split(',', QString::SkipEmptyParts);
#endif #endif
QList<int> out; QList<int> out;
for (int i = 0; i < split.size(); ++i) for (int i = 0; i < split.size(); ++i) {
{
out.append(split.at(i).toInt()); out.append(split.at(i).toInt());
} }
return out; return out;
@ -912,8 +845,7 @@ QList<int> stringToIntList(const QString &string)
QString intListToString(const QList<int>& list) QString intListToString(const QList<int>& list)
{ {
QStringList slist; QStringList slist;
for (int i = 0; i < list.size(); ++i) for (int i = 0; i < list.size(); ++i) {
{
slist.append(QString::number(list.at(i))); slist.append(QString::number(list.at(i)));
} }
return slist.join(','); return slist.join(',');
@ -927,40 +859,23 @@ void MainWindow::onCatToggled(bool state)
void MainWindow::setCatBackground(bool enabled) void MainWindow::setCatBackground(bool enabled)
{ {
if (enabled) { view->setPaintCat(enabled);
view->setStyleSheet(QString(R"( view->viewport()->repaint();
InstanceView
{
background-image: url(:/backgrounds/%1);
background-attachment: fixed;
background-clip: padding;
background-position: bottom right;
background-repeat: none;
background-color:palette(base);
})")
.arg(ThemeManager::getCatImage()));
} else {
view->setStyleSheet(QString());
}
} }
void MainWindow::runModalTask(Task* task) void MainWindow::runModalTask(Task* task)
{ {
connect(task, &Task::failed, [this](QString reason) connect(task, &Task::failed,
{ [this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); });
CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); connect(task, &Task::succeeded, [this, task]() {
});
connect(task, &Task::succeeded, [this, task]()
{
QStringList warnings = task->warnings(); QStringList warnings = task->warnings();
if(warnings.count()) if (warnings.count()) {
{
CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show();
} }
}); });
connect(task, &Task::aborted, [this] connect(task, &Task::aborted, [this] {
{ CustomMessageBox::selectable(this, tr("Task aborted"), tr("The task has been aborted by the user."), QMessageBox::Information)
CustomMessageBox::selectable(this, tr("Task aborted"), tr("The task has been aborted by the user."), QMessageBox::Information)->show(); ->show();
}); });
ProgressDialog loadDialog(this); ProgressDialog loadDialog(this);
loadDialog.setSkipButton(true, tr("Abort")); loadDialog.setSkipButton(true, tr("Abort"));
@ -994,38 +909,30 @@ void MainWindow::finalizeInstance(InstancePtr inst)
{ {
view->updateGeometries(); view->updateGeometries();
setSelectedInstanceById(inst->id()); setSelectedInstanceById(inst->id());
if (APPLICATION->accounts()->anyAccountIsValid()) if (APPLICATION->accounts()->anyAccountIsValid()) {
{
ProgressDialog loadDialog(this); ProgressDialog loadDialog(this);
auto update = inst->createUpdateTask(Net::Mode::Online); auto update = inst->createUpdateTask(Net::Mode::Online);
connect(update.get(), &Task::failed, [this](QString reason) connect(update.get(), &Task::failed, [this](QString reason) {
{
QString error = QString("Instance load failed: %1").arg(reason); QString error = QString("Instance load failed: %1").arg(reason);
CustomMessageBox::selectable(this, tr("Error"), error, QMessageBox::Warning)->show(); CustomMessageBox::selectable(this, tr("Error"), error, QMessageBox::Warning)->show();
}); });
if(update) if (update) {
{
loadDialog.setSkipButton(true, tr("Abort")); loadDialog.setSkipButton(true, tr("Abort"));
loadDialog.execWithTask(update.get()); loadDialog.execWithTask(update.get());
} }
} } else {
else CustomMessageBox::selectable(this, tr("Error"),
{
CustomMessageBox::selectable(
this,
tr("Error"),
tr("The launcher cannot download Minecraft or update instances unless you have at least " tr("The launcher cannot download Minecraft or update instances unless you have at least "
"one account added.\nPlease add your Mojang or Minecraft account."), "one account added.\nPlease add your Mojang or Minecraft account."),
QMessageBox::Warning QMessageBox::Warning)
)->show(); ->show();
} }
} }
void MainWindow::addInstance(QString url) void MainWindow::addInstance(QString url)
{ {
QString groupName; QString groupName;
do do {
{
QObject* obj = sender(); QObject* obj = sender();
if (!obj) if (!obj)
break; break;
@ -1038,8 +945,7 @@ void MainWindow::addInstance(QString url)
groupName = map["group"].toString(); groupName = map["group"].toString();
} while (0); } while (0);
if(groupName.isEmpty()) if (groupName.isEmpty()) {
{
groupName = APPLICATION->settings()->get("LastUsedGroupForNewInstance").toString(); groupName = APPLICATION->settings()->get("LastUsedGroupForNewInstance").toString();
} }
@ -1050,8 +956,7 @@ void MainWindow::addInstance(QString url)
APPLICATION->settings()->set("LastUsedGroupForNewInstance", newInstDlg.instGroup()); APPLICATION->settings()->set("LastUsedGroupForNewInstance", newInstDlg.instGroup());
InstanceTask* creationTask = newInstDlg.extractTask(); InstanceTask* creationTask = newInstDlg.extractTask();
if(creationTask) if (creationTask) {
{
instanceFromInstanceTask(creationTask); instanceFromInstanceTask(creationTask);
} }
} }
@ -1145,8 +1050,7 @@ void MainWindow::on_actionChangeInstIcon_triggered()
IconPickerDialog dlg(this); IconPickerDialog dlg(this);
dlg.execWithSelection(m_selectedInstance->iconKey()); dlg.execWithSelection(m_selectedInstance->iconKey());
if (dlg.result() == QDialog::Accepted) if (dlg.result() == QDialog::Accepted) {
{
m_selectedInstance->setIconKey(dlg.selectedIconKey); m_selectedInstance->setIconKey(dlg.selectedIconKey);
auto icon = APPLICATION->icons()->getIcon(dlg.selectedIconKey); auto icon = APPLICATION->icons()->getIcon(dlg.selectedIconKey);
ui->actionChangeInstIcon->setIcon(icon); ui->actionChangeInstIcon->setIcon(icon);
@ -1156,8 +1060,7 @@ void MainWindow::on_actionChangeInstIcon_triggered()
void MainWindow::iconUpdated(QString icon) void MainWindow::iconUpdated(QString icon)
{ {
if (icon == m_currentInstIcon) if (icon == m_currentInstIcon) {
{
auto icon = APPLICATION->icons()->getIcon(m_currentInstIcon); auto icon = APPLICATION->icons()->getIcon(m_currentInstIcon);
ui->actionChangeInstIcon->setIcon(icon); ui->actionChangeInstIcon->setIcon(icon);
changeIconButton->setIcon(icon); changeIconButton->setIcon(icon);
@ -1177,8 +1080,7 @@ void MainWindow::setSelectedInstanceById(const QString &id)
if (id.isNull()) if (id.isNull())
return; return;
const QModelIndex index = APPLICATION->instances()->getInstanceIndexById(id); const QModelIndex index = APPLICATION->instances()->getInstanceIndexById(id);
if (index.isValid()) if (index.isValid()) {
{
QModelIndex selectionIndex = proxymodel->mapFromSource(index); QModelIndex selectionIndex = proxymodel->mapFromSource(index);
view->selectionModel()->setCurrentIndex(selectionIndex, QItemSelectionModel::ClearAndSelect); view->selectionModel()->setCurrentIndex(selectionIndex, QItemSelectionModel::ClearAndSelect);
updateStatusCenter(); updateStatusCenter();
@ -1200,8 +1102,7 @@ void MainWindow::on_actionChangeInstGroup_triggered()
name = QInputDialog::getItem(this, tr("Group name"), tr("Enter a new group name."), groups, foo, true, &ok); name = QInputDialog::getItem(this, tr("Group name"), tr("Enter a new group name."), groups, foo, true, &ok);
name = name.simplified(); name = name.simplified();
if (ok) if (ok) {
{
APPLICATION->instances()->setInstanceGroup(instId, name); APPLICATION->instances()->setInstanceGroup(instId, name);
} }
} }
@ -1218,12 +1119,10 @@ void MainWindow::deleteGroup()
if (!map.contains("group")) if (!map.contains("group"))
return; return;
QString groupName = map["group"].toString(); QString groupName = map["group"].toString();
if(!groupName.isEmpty()) if (!groupName.isEmpty()) {
{ auto reply = QMessageBox::question(this, tr("Delete group"), tr("Are you sure you want to delete the group %1?").arg(groupName),
auto reply = QMessageBox::question(this, tr("Delete group"), tr("Are you sure you want to delete the group %1?") QMessageBox::Yes | QMessageBox::No);
.arg(groupName), QMessageBox::Yes | QMessageBox::No); if (reply == QMessageBox::Yes) {
if(reply == QMessageBox::Yes)
{
APPLICATION->instances()->deleteGroup(groupName); APPLICATION->instances()->deleteGroup(groupName);
} }
} }
@ -1259,12 +1158,9 @@ void MainWindow::on_actionViewCentralModsFolder_triggered()
void MainWindow::checkForUpdates() void MainWindow::checkForUpdates()
{ {
if(BuildConfig.UPDATER_ENABLED) if (BuildConfig.UPDATER_ENABLED) {
{
APPLICATION->triggerUpdateCheck(); APPLICATION->triggerUpdateCheck();
} } else {
else
{
qWarning() << "Updater not set up. Cannot check for updates."; qWarning() << "Updater not set up. Cannot check for updates.";
} }
} }
@ -1292,7 +1188,17 @@ void MainWindow::globalSettingsClosed()
void MainWindow::on_actionEditInstance_triggered() void MainWindow::on_actionEditInstance_triggered()
{ {
if (!m_selectedInstance)
return;
if (m_selectedInstance->canEdit()) {
APPLICATION->showInstanceWindow(m_selectedInstance); APPLICATION->showInstanceWindow(m_selectedInstance);
} else {
CustomMessageBox::selectable(this, tr("Instance not editable"),
tr("This instance is not editable. It may be broken, invalid, or too old. Check logs for details."),
QMessageBox::Critical)
->show();
}
} }
void MainWindow::on_actionManageAccounts_triggered() void MainWindow::on_actionManageAccounts_triggered()
@ -1354,7 +1260,8 @@ void MainWindow::newsButtonClicked()
news_dialog.exec(); news_dialog.exec();
} }
void MainWindow::onCatChanged(int) { void MainWindow::onCatChanged(int)
{
setCatBackground(APPLICATION->settings()->get("TheCat").toBool()); setCatBackground(APPLICATION->settings()->get("TheCat").toBool());
} }
@ -1385,14 +1292,15 @@ void MainWindow::on_actionDeleteInstance_triggered()
auto linkedInstances = APPLICATION->instances()->getLinkedInstancesById(id); auto linkedInstances = APPLICATION->instances()->getLinkedInstancesById(id);
if (!linkedInstances.empty()) { if (!linkedInstances.empty()) {
response = CustomMessageBox::selectable( response = CustomMessageBox::selectable(this, tr("There are linked instances"),
this, tr("There are linked instances"),
tr("The following instance(s) might reference files in this instance:\n\n" tr("The following instance(s) might reference files in this instance:\n\n"
"%1\n\n" "%1\n\n"
"Deleting it could break the other instance(s), \n\n" "Deleting it could break the other instance(s), \n\n"
"Do you wish to proceed?", nullptr, linkedInstances.count()).arg(linkedInstances.join("\n")), "Do you wish to proceed?",
QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No nullptr, linkedInstances.count())
)->exec(); .arg(linkedInstances.join("\n")),
QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No)
->exec();
if (response != QMessageBox::Yes) if (response != QMessageBox::Yes)
return; return;
} }
@ -1407,8 +1315,7 @@ void MainWindow::on_actionDeleteInstance_triggered()
void MainWindow::on_actionExportInstanceZip_triggered() void MainWindow::on_actionExportInstanceZip_triggered()
{ {
if (m_selectedInstance) if (m_selectedInstance) {
{
ExportInstanceDialog dlg(m_selectedInstance, this); ExportInstanceDialog dlg(m_selectedInstance, this);
dlg.exec(); dlg.exec();
} }
@ -1416,25 +1323,54 @@ void MainWindow::on_actionExportInstanceZip_triggered()
void MainWindow::on_actionExportInstanceMrPack_triggered() void MainWindow::on_actionExportInstanceMrPack_triggered()
{ {
if (m_selectedInstance) if (m_selectedInstance) {
{ ExportPackDialog dlg(m_selectedInstance, this);
ExportMrPackDialog dlg(m_selectedInstance, this);
dlg.exec(); dlg.exec();
} }
} }
void MainWindow::on_actionExportInstanceToModList_triggered()
{
if (m_selectedInstance) {
ExportToModListDialog dlg(m_selectedInstance, this);
dlg.exec();
}
}
void MainWindow::on_actionExportInstanceFlamePack_triggered()
{
if (m_selectedInstance) {
auto instance = dynamic_cast<MinecraftInstance*>(m_selectedInstance.get());
if (instance) {
QString errorMsg;
if (instance->getPackProfile()->getComponent("org.quiltmc.quilt-loader")) {
errorMsg = tr("Quilt is currently not supported by CurseForge modpacks.");
} else if (auto cmp = instance->getPackProfile()->getComponent("net.minecraft");
cmp && cmp->getVersionFile() && cmp->getVersionFile()->type == "snapshot") {
errorMsg = tr("Snapshots are currently not supported by CurseForge modpacks.");
}
if (!errorMsg.isEmpty()) {
QMessageBox msgBox;
msgBox.setText(errorMsg);
msgBox.exec();
return;
}
ExportPackDialog dlg(m_selectedInstance, this, ModPlatform::ResourceProvider::FLAME);
dlg.exec();
}
}
}
void MainWindow::on_actionRenameInstance_triggered() void MainWindow::on_actionRenameInstance_triggered()
{ {
if (m_selectedInstance) if (m_selectedInstance) {
{
view->edit(view->currentIndex()); view->edit(view->currentIndex());
} }
} }
void MainWindow::on_actionViewSelectedInstFolder_triggered() void MainWindow::on_actionViewSelectedInstFolder_triggered()
{ {
if (m_selectedInstance) if (m_selectedInstance) {
{
QString str = m_selectedInstance->instanceRoot(); QString str = m_selectedInstance->instanceRoot();
DesktopServices::openDirectory(QDir(str).absolutePath()); DesktopServices::openDirectory(QDir(str).absolutePath());
} }
@ -1452,8 +1388,7 @@ void MainWindow::closeEvent(QCloseEvent *event)
void MainWindow::changeEvent(QEvent* event) void MainWindow::changeEvent(QEvent* event)
{ {
if (event->type() == QEvent::LanguageChange) if (event->type() == QEvent::LanguageChange) {
{
retranslateUi(); retranslateUi();
} }
QMainWindow::changeEvent(event); QMainWindow::changeEvent(event);
@ -1473,8 +1408,7 @@ void MainWindow::instanceActivated(QModelIndex index)
void MainWindow::on_actionLaunchInstance_triggered() void MainWindow::on_actionLaunchInstance_triggered()
{ {
if(m_selectedInstance && !m_selectedInstance->isRunning()) if (m_selectedInstance && !m_selectedInstance->isRunning()) {
{
APPLICATION->launch(m_selectedInstance); APPLICATION->launch(m_selectedInstance);
} }
} }
@ -1486,24 +1420,21 @@ void MainWindow::activateInstance(InstancePtr instance)
void MainWindow::on_actionLaunchInstanceOffline_triggered() void MainWindow::on_actionLaunchInstanceOffline_triggered()
{ {
if (m_selectedInstance) if (m_selectedInstance) {
{
APPLICATION->launch(m_selectedInstance, false); APPLICATION->launch(m_selectedInstance, false);
} }
} }
void MainWindow::on_actionLaunchInstanceDemo_triggered() void MainWindow::on_actionLaunchInstanceDemo_triggered()
{ {
if (m_selectedInstance) if (m_selectedInstance) {
{
APPLICATION->launch(m_selectedInstance, false, true); APPLICATION->launch(m_selectedInstance, false, true);
} }
} }
void MainWindow::on_actionKillInstance_triggered() void MainWindow::on_actionKillInstance_triggered()
{ {
if(m_selectedInstance && m_selectedInstance->isRunning()) if (m_selectedInstance && m_selectedInstance->isRunning()) {
{
APPLICATION->kill(m_selectedInstance); APPLICATION->kill(m_selectedInstance);
} }
} }
@ -1524,11 +1455,36 @@ void MainWindow::on_actionCreateInstanceShortcut_triggered()
QString iconPath; QString iconPath;
QStringList args; QStringList args;
#if defined(Q_OS_MACOS) #if defined(Q_OS_MACOS)
appPath = QApplication::applicationFilePath();
if (appPath.startsWith("/private/var/")) { if (appPath.startsWith("/private/var/")) {
QMessageBox::critical(this, tr("Create instance shortcut"), QMessageBox::critical(this, tr("Create instance shortcut"),
tr("The launcher is in the folder it was extracted from, therefore it cannot create shortcuts.")); tr("The launcher is in the folder it was extracted from, therefore it cannot create shortcuts."));
return; return;
} }
auto pIcon = APPLICATION->icons()->icon(m_selectedInstance->iconKey());
if (pIcon == nullptr) {
pIcon = APPLICATION->icons()->icon("grass");
}
iconPath = FS::PathCombine(m_selectedInstance->instanceRoot(), "Icon.icns");
QFile iconFile(iconPath);
if (!iconFile.open(QFile::WriteOnly)) {
QMessageBox::critical(this, tr("Create instance Application"), tr("Failed to create icon for Application."));
return;
}
QIcon icon = pIcon->icon();
bool success = icon.pixmap(1024, 1024).save(iconPath, "ICNS");
iconFile.close();
if (!success) {
iconFile.remove();
QMessageBox::critical(this, tr("Create instance Application"), tr("Failed to create icon for Application."));
return;
}
#elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) #elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
if (appPath.startsWith("/tmp/.mount_")) { if (appPath.startsWith("/tmp/.mount_")) {
// AppImage! // AppImage!
@ -1611,7 +1567,11 @@ void MainWindow::on_actionCreateInstanceShortcut_triggered()
#endif #endif
args.append({ "--launch", m_selectedInstance->id() }); args.append({ "--launch", m_selectedInstance->id() });
if (FS::createShortcut(desktopFilePath, appPath, args, m_selectedInstance->name(), iconPath)) { if (FS::createShortcut(desktopFilePath, appPath, args, m_selectedInstance->name(), iconPath)) {
#if not defined(Q_OS_MACOS)
QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance on your desktop!")); QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance on your desktop!"));
#else
QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance!"));
#endif
} else { } else {
#if not defined(Q_OS_MACOS) #if not defined(Q_OS_MACOS)
iconFile.remove(); iconFile.remove();
@ -1638,8 +1598,7 @@ void MainWindow::startTask(Task *task)
void MainWindow::instanceChanged(const QModelIndex& current, const QModelIndex& previous) void MainWindow::instanceChanged(const QModelIndex& current, const QModelIndex& previous)
{ {
if (!current.isValid()) if (!current.isValid()) {
{
APPLICATION->settings()->set("SelectedInstance", QString()); APPLICATION->settings()->set("SelectedInstance", QString());
selectionBad(); selectionBad();
return; return;
@ -1649,8 +1608,7 @@ void MainWindow::instanceChanged(const QModelIndex &current, const QModelIndex &
} }
QString id = current.data(InstanceList::InstanceIDRole).toString(); QString id = current.data(InstanceList::InstanceIDRole).toString();
m_selectedInstance = APPLICATION->instances()->getInstanceById(id); m_selectedInstance = APPLICATION->instances()->getInstanceById(id);
if (m_selectedInstance) if (m_selectedInstance) {
{
ui->instanceToolBar->setEnabled(true); ui->instanceToolBar->setEnabled(true);
setInstanceActionsEnabled(true); setInstanceActionsEnabled(true);
ui->actionLaunchInstance->setEnabled(m_selectedInstance->canLaunch()); ui->actionLaunchInstance->setEnabled(m_selectedInstance->canLaunch());
@ -1675,9 +1633,7 @@ void MainWindow::instanceChanged(const QModelIndex &current, const QModelIndex &
APPLICATION->settings()->set("SelectedInstance", m_selectedInstance->id()); APPLICATION->settings()->set("SelectedInstance", m_selectedInstance->id());
connect(m_selectedInstance.get(), &BaseInstance::runningStatusChanged, this, &MainWindow::refreshCurrentInstance); connect(m_selectedInstance.get(), &BaseInstance::runningStatusChanged, this, &MainWindow::refreshCurrentInstance);
} } else {
else
{
ui->instanceToolBar->setEnabled(false); ui->instanceToolBar->setEnabled(false);
setInstanceActionsEnabled(false); setInstanceActionsEnabled(false);
ui->actionLaunchInstance->setEnabled(false); ui->actionLaunchInstance->setEnabled(false);
@ -1699,8 +1655,7 @@ void MainWindow::instanceDataChanged(const QModelIndex &topLeft, const QModelInd
{ {
auto current = view->selectionModel()->currentIndex(); auto current = view->selectionModel()->currentIndex();
QItemSelection test(topLeft, bottomRight); QItemSelection test(topLeft, bottomRight);
if (test.contains(current)) if (test.contains(current)) {
{
instanceChanged(current, current); instanceChanged(current, current);
} }
} }
@ -1724,34 +1679,28 @@ void MainWindow::selectionBad()
void MainWindow::checkInstancePathForProblems() void MainWindow::checkInstancePathForProblems()
{ {
QString instanceFolder = APPLICATION->settings()->get("InstanceDir").toString(); QString instanceFolder = APPLICATION->settings()->get("InstanceDir").toString();
if (FS::checkProblemticPathJava(QDir(instanceFolder))) if (FS::checkProblemticPathJava(QDir(instanceFolder))) {
{
QMessageBox warning(this); QMessageBox warning(this);
warning.setText(tr("Your instance folder contains \'!\' and this is known to cause Java problems!")); warning.setText(tr("Your instance folder contains \'!\' and this is known to cause Java problems!"));
warning.setInformativeText( warning.setInformativeText(tr("You have now two options: <br/>"
tr(
"You have now two options: <br/>"
" - change the instance folder in the settings <br/>" " - change the instance folder in the settings <br/>"
" - move this installation of %1 to a different folder" " - move this installation of %1 to a different folder")
).arg(BuildConfig.LAUNCHER_DISPLAYNAME) .arg(BuildConfig.LAUNCHER_DISPLAYNAME));
);
warning.setDefaultButton(QMessageBox::Ok); warning.setDefaultButton(QMessageBox::Ok);
warning.exec(); warning.exec();
} }
auto tempFolderText = tr("This is a problem: <br/>" auto tempFolderText =
tr("This is a problem: <br/>"
" - The launcher will likely be deleted without warning by the operating system <br/>" " - The launcher will likely be deleted without warning by the operating system <br/>"
" - close the launcher now and extract it to a real location, not a temporary folder"); " - close the launcher now and extract it to a real location, not a temporary folder");
QString pathfoldername = QDir(instanceFolder).absolutePath(); QString pathfoldername = QDir(instanceFolder).absolutePath();
if (pathfoldername.contains("Rar$", Qt::CaseInsensitive)) if (pathfoldername.contains("Rar$", Qt::CaseInsensitive)) {
{
QMessageBox warning(this); QMessageBox warning(this);
warning.setText(tr("Your instance folder contains \'Rar$\' - that means you haven't extracted the launcher archive!")); warning.setText(tr("Your instance folder contains \'Rar$\' - that means you haven't extracted the launcher archive!"));
warning.setInformativeText(tempFolderText); warning.setInformativeText(tempFolderText);
warning.setDefaultButton(QMessageBox::Ok); warning.setDefaultButton(QMessageBox::Ok);
warning.exec(); warning.exec();
} } else if (pathfoldername.startsWith(QDir::tempPath()) || pathfoldername.contains("/TempState/")) {
else if (pathfoldername.startsWith(QDir::tempPath()) || pathfoldername.contains("/TempState/"))
{
QMessageBox warning(this); QMessageBox warning(this);
warning.setText(tr("Your instance folder is in a temporary folder: \'%1\'!").arg(QDir::tempPath())); warning.setText(tr("Your instance folder is in a temporary folder: \'%1\'!").arg(QDir::tempPath()));
warning.setInformativeText(tempFolderText); warning.setInformativeText(tempFolderText);

View File

@ -157,6 +157,8 @@ private slots:
inline void on_actionExportInstance_triggered() { on_actionExportInstanceZip_triggered(); } inline void on_actionExportInstance_triggered() { on_actionExportInstanceZip_triggered(); }
void on_actionExportInstanceZip_triggered(); void on_actionExportInstanceZip_triggered();
void on_actionExportInstanceMrPack_triggered(); void on_actionExportInstanceMrPack_triggered();
void on_actionExportInstanceFlamePack_triggered();
void on_actionExportInstanceToModList_triggered();
void on_actionRenameInstance_triggered(); void on_actionRenameInstance_triggered();

View File

@ -479,6 +479,22 @@
<string>Modrinth (mrpack)</string> <string>Modrinth (mrpack)</string>
</property> </property>
</action> </action>
<action name="actionExportInstanceFlamePack">
<property name="icon">
<iconset theme="flame"/>
</property>
<property name="text">
<string>CurseForge (zip)</string>
</property>
</action>
<action name="actionExportInstanceToModList">
<property name="icon">
<iconset theme="new"/>
</property>
<property name="text">
<string>Mod List</string>
</property>
</action>
<action name="actionCreateInstanceShortcut"> <action name="actionCreateInstanceShortcut">
<property name="icon"> <property name="icon">
<iconset theme="shortcut"> <iconset theme="shortcut">

View File

@ -3,6 +3,7 @@
* Prism Launcher - Minecraft Launcher * Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me> * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -35,22 +36,27 @@
*/ */
#include "ExportInstanceDialog.h" #include "ExportInstanceDialog.h"
#include "ui_ExportInstanceDialog.h"
#include <BaseInstance.h> #include <BaseInstance.h>
#include <MMCZip.h> #include <MMCZip.h>
#include <QFileDialog> #include <QFileDialog>
#include <QMessageBox>
#include <QFileSystemModel> #include <QFileSystemModel>
#include <QMessageBox>
#include "FileIgnoreProxy.h"
#include "QObjectPtr.h"
#include "ui/dialogs/CustomMessageBox.h"
#include "ui/dialogs/ProgressDialog.h"
#include "ui_ExportInstanceDialog.h"
#include <QSortFilterProxyModel>
#include <QDebug>
#include <QSaveFile>
#include <QStack>
#include <QFileInfo>
#include "SeparatorPrefixTree.h"
#include "Application.h"
#include <icons/IconList.h>
#include <FileSystem.h> #include <FileSystem.h>
#include <icons/IconList.h>
#include <QDebug>
#include <QFileInfo>
#include <QSaveFile>
#include <QSortFilterProxyModel>
#include <QStack>
#include <functional>
#include "Application.h"
#include "SeparatorPrefixTree.h"
ExportInstanceDialog::ExportInstanceDialog(InstancePtr instance, QWidget* parent) ExportInstanceDialog::ExportInstanceDialog(InstancePtr instance, QWidget* parent)
: QDialog(parent), ui(new Ui::ExportInstanceDialog), m_instance(instance) : QDialog(parent), ui(new Ui::ExportInstanceDialog), m_instance(instance)
@ -60,8 +66,14 @@ ExportInstanceDialog::ExportInstanceDialog(InstancePtr instance, QWidget *parent
model->setIconProvider(&icons); model->setIconProvider(&icons);
auto root = instance->instanceRoot(); auto root = instance->instanceRoot();
proxyModel = new FileIgnoreProxy(root, this); proxyModel = new FileIgnoreProxy(root, this);
loadPackIgnore();
proxyModel->setSourceModel(model); proxyModel->setSourceModel(model);
auto prefix = QDir(instance->instanceRoot()).relativeFilePath(instance->gameRoot());
proxyModel->ignoreFilesWithPath().insert({ FS::PathCombine(prefix, "logs"), FS::PathCombine(prefix, "crash-reports") });
proxyModel->ignoreFilesWithName().append({ ".DS_Store", "thumbs.db", "Thumbs.db" });
proxyModel->ignoreFilesWithPath().insert(
{ FS::PathCombine(prefix, ".cache"), FS::PathCombine(prefix, ".fabric"), FS::PathCombine(prefix, ".quilt") });
loadPackIgnore();
ui->treeView->setModel(proxyModel); ui->treeView->setModel(proxyModel);
ui->treeView->setRootIndex(proxyModel->mapFromSource(model->index(root))); ui->treeView->setRootIndex(proxyModel->mapFromSource(model->index(root)));
ui->treeView->sortByColumn(0, Qt::AscendingOrder); ui->treeView->sortByColumn(0, Qt::AscendingOrder);
@ -98,20 +110,14 @@ void SaveIcon(InstancePtr m_instance)
auto& image = mmcIcon->m_images[mmcIcon->type()]; auto& image = mmcIcon->m_images[mmcIcon->type()];
auto& icon = image.icon; auto& icon = image.icon;
auto sizes = icon.availableSizes(); auto sizes = icon.availableSizes();
if(sizes.size() == 0) if (sizes.size() == 0) {
{
return; return;
} }
auto areaOf = [](QSize size) auto areaOf = [](QSize size) { return size.width() * size.height(); };
{
return size.width() * size.height();
};
QSize largest = sizes[0]; QSize largest = sizes[0];
// find variant with largest area // find variant with largest area
for(auto size: sizes) for (auto size : sizes) {
{ if (areaOf(largest) < areaOf(size)) {
if(areaOf(largest) < areaOf(size))
{
largest = size; largest = size;
} }
} }
@ -119,66 +125,57 @@ void SaveIcon(InstancePtr m_instance)
pixmap.save(FS::PathCombine(m_instance->instanceRoot(), iconKey + ".png")); pixmap.save(FS::PathCombine(m_instance->instanceRoot(), iconKey + ".png"));
} }
bool ExportInstanceDialog::doExport() void ExportInstanceDialog::doExport()
{ {
auto name = FS::RemoveInvalidFilenameChars(m_instance->name()); auto name = FS::RemoveInvalidFilenameChars(m_instance->name());
const QString output = QFileDialog::getSaveFileName( const QString output = QFileDialog::getSaveFileName(this, tr("Export %1").arg(m_instance->name()),
this, tr("Export %1").arg(m_instance->name()),
FS::PathCombine(QDir::homePath(), name + ".zip"), "Zip (*.zip)", nullptr); FS::PathCombine(QDir::homePath(), name + ".zip"), "Zip (*.zip)", nullptr);
if (output.isEmpty()) if (output.isEmpty()) {
{ QDialog::done(QDialog::Rejected);
return false; return;
} }
SaveIcon(m_instance); SaveIcon(m_instance);
auto & blocked = proxyModel->blockedPaths();
using std::placeholders::_1;
auto files = QFileInfoList(); auto files = QFileInfoList();
if (!MMCZip::collectFileListRecursively(m_instance->instanceRoot(), nullptr, &files, if (!MMCZip::collectFileListRecursively(m_instance->instanceRoot(), nullptr, &files,
std::bind(&SeparatorPrefixTree<'/'>::covers, blocked, _1))) { std::bind(&FileIgnoreProxy::filterFile, proxyModel, std::placeholders::_1))) {
QMessageBox::warning(this, tr("Error"), tr("Unable to export instance")); QMessageBox::warning(this, tr("Error"), tr("Unable to export instance"));
return false; QDialog::done(QDialog::Rejected);
return;
} }
if (!MMCZip::compressDirFiles(output, m_instance->instanceRoot(), files, true)) auto task = makeShared<MMCZip::ExportToZipTask>(output, m_instance->instanceRoot(), files, "", true);
{
QMessageBox::warning(this, tr("Error"), tr("Unable to export instance")); connect(task.get(), &Task::failed, this,
return false; [this, output](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); });
} connect(task.get(), &Task::finished, this, [task] { task->deleteLater(); });
return true;
ProgressDialog progress(this);
progress.setSkipButton(true, tr("Abort"));
auto result = progress.execWithTask(task.get());
QDialog::done(result);
} }
void ExportInstanceDialog::done(int result) void ExportInstanceDialog::done(int result)
{ {
savePackIgnore(); savePackIgnore();
if (result == QDialog::Accepted) if (result == QDialog::Accepted) {
{ doExport();
if (doExport())
{
QDialog::done(QDialog::Accepted);
return; return;
} }
else
{
return;
}
}
QDialog::done(result); QDialog::done(result);
} }
void ExportInstanceDialog::rowsInserted(QModelIndex parent, int top, int bottom) void ExportInstanceDialog::rowsInserted(QModelIndex parent, int top, int bottom)
{ {
// WARNING: possible off-by-one? // WARNING: possible off-by-one?
for(int i = top; i < bottom; i++) for (int i = top; i < bottom; i++) {
{
auto node = proxyModel->index(i, 0, parent); auto node = proxyModel->index(i, 0, parent);
if(proxyModel->shouldExpand(node)) if (proxyModel->shouldExpand(node)) {
{
auto expNode = node.parent(); auto expNode = node.parent();
if(!expNode.isValid()) if (!expNode.isValid()) {
{
continue; continue;
} }
ui->treeView->expand(node); ui->treeView->expand(node);
@ -195,8 +192,7 @@ void ExportInstanceDialog::loadPackIgnore()
{ {
auto filename = ignoreFileName(); auto filename = ignoreFileName();
QFile ignoreFile(filename); QFile ignoreFile(filename);
if(!ignoreFile.open(QIODevice::ReadOnly)) if (!ignoreFile.open(QIODevice::ReadOnly)) {
{
return; return;
} }
auto data = ignoreFile.readAll(); auto data = ignoreFile.readAll();
@ -212,12 +208,9 @@ void ExportInstanceDialog::savePackIgnore()
{ {
auto data = proxyModel->blockedPaths().toStringList().join('\n').toUtf8(); auto data = proxyModel->blockedPaths().toStringList().join('\n').toUtf8();
auto filename = ignoreFileName(); auto filename = ignoreFileName();
try try {
{
FS::write(filename, data); FS::write(filename, data);
} } catch (const Exception& e) {
catch (const Exception &e)
{
qWarning() << e.cause(); qWarning() << e.cause();
} }
} }

View File

@ -2,6 +2,7 @@
/* /*
* Prism Launcher - Minecraft Launcher * Prism Launcher - Minecraft Launcher
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me> * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -38,19 +39,17 @@
#include <QDialog> #include <QDialog>
#include <QModelIndex> #include <QModelIndex>
#include <memory> #include <memory>
#include "FileIgnoreProxy.h"
#include "FastFileIconProvider.h" #include "FastFileIconProvider.h"
#include "FileIgnoreProxy.h"
class BaseInstance; class BaseInstance;
typedef std::shared_ptr<BaseInstance> InstancePtr; typedef std::shared_ptr<BaseInstance> InstancePtr;
namespace Ui namespace Ui {
{
class ExportInstanceDialog; class ExportInstanceDialog;
} }
class ExportInstanceDialog : public QDialog class ExportInstanceDialog : public QDialog {
{
Q_OBJECT Q_OBJECT
public: public:
@ -60,7 +59,7 @@ public:
virtual void done(int result); virtual void done(int result);
private: private:
bool doExport(); void doExport();
void loadPackIgnore(); void loadPackIgnore();
void savePackIgnore(); void savePackIgnore();
QString ignoreFileName(); QString ignoreFileName();

View File

@ -16,11 +16,13 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
#include "ExportMrPackDialog.h" #include "ExportPackDialog.h"
#include "minecraft/mod/ModFolderModel.h" #include "minecraft/mod/ModFolderModel.h"
#include "modplatform/ModIndex.h"
#include "modplatform/flame/FlamePackExportTask.h"
#include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/CustomMessageBox.h"
#include "ui/dialogs/ProgressDialog.h" #include "ui/dialogs/ProgressDialog.h"
#include "ui_ExportMrPackDialog.h" #include "ui_ExportPackDialog.h"
#include <QFileDialog> #include <QFileDialog>
#include <QFileSystemModel> #include <QFileSystemModel>
@ -32,17 +34,24 @@
#include "MMCZip.h" #include "MMCZip.h"
#include "modplatform/modrinth/ModrinthPackExportTask.h" #include "modplatform/modrinth/ModrinthPackExportTask.h"
ExportMrPackDialog::ExportMrPackDialog(InstancePtr instance, QWidget* parent) ExportPackDialog::ExportPackDialog(InstancePtr instance, QWidget* parent, ModPlatform::ResourceProvider provider)
: QDialog(parent), instance(instance), ui(new Ui::ExportMrPackDialog) : QDialog(parent), instance(instance), ui(new Ui::ExportPackDialog), m_provider(provider)
{ {
ui->setupUi(this); ui->setupUi(this);
ui->name->setText(instance->name()); ui->name->setText(instance->name());
if (m_provider == ModPlatform::ResourceProvider::MODRINTH) {
ui->summary->setText(instance->notes().split(QRegularExpression("\\r?\\n"))[0]); ui->summary->setText(instance->notes().split(QRegularExpression("\\r?\\n"))[0]);
setWindowTitle("Export Modrinth Pack");
} else {
setWindowTitle("Export CurseForge Pack");
ui->version->setText("");
ui->summaryLabel->setText("Author");
}
// ensure a valid pack is generated // ensure a valid pack is generated
// the name and version fields mustn't be empty // the name and version fields mustn't be empty
connect(ui->name, &QLineEdit::textEdited, this, &ExportMrPackDialog::validate); connect(ui->name, &QLineEdit::textEdited, this, &ExportPackDialog::validate);
connect(ui->version, &QLineEdit::textEdited, this, &ExportMrPackDialog::validate); connect(ui->version, &QLineEdit::textEdited, this, &ExportPackDialog::validate);
// the instance name can technically be empty // the instance name can technically be empty
validate(); validate();
@ -52,8 +61,9 @@ ExportMrPackDialog::ExportMrPackDialog(InstancePtr instance, QWidget* parent)
// use the game root - everything outside cannot be exported // use the game root - everything outside cannot be exported
const QDir root(instance->gameRoot()); const QDir root(instance->gameRoot());
proxy = new FileIgnoreProxy(instance->gameRoot(), this); proxy = new FileIgnoreProxy(instance->gameRoot(), this);
proxy->ignoreFilesWithPath().insert({ "logs", "crash-reports", ".cache", ".fabric", ".quilt" });
proxy->ignoreFilesWithName().append({ ".DS_Store", "thumbs.db", "Thumbs.db" });
proxy->setSourceModel(model); proxy->setSourceModel(model);
proxy->setFilterRegularExpression("^(?!(\\.DS_Store)|([tT]humbs\\.db)).+$");
const QDir::Filters filter(QDir::AllEntries | QDir::NoDotAndDotDot | QDir::AllDirs | QDir::Hidden); const QDir::Filters filter(QDir::AllEntries | QDir::NoDotAndDotDot | QDir::AllDirs | QDir::Hidden);
@ -65,6 +75,7 @@ ExportMrPackDialog::ExportMrPackDialog(InstancePtr instance, QWidget* parent)
MinecraftInstance* mcInstance = dynamic_cast<MinecraftInstance*>(instance.get()); MinecraftInstance* mcInstance = dynamic_cast<MinecraftInstance*>(instance.get());
if (mcInstance) { if (mcInstance) {
mcInstance->loaderModList()->update();
const QDir index = mcInstance->loaderModList()->indexDir(); const QDir index = mcInstance->loaderModList()->indexDir();
if (index.exists()) if (index.exists())
proxy->blockedPaths().insert(root.relativeFilePath(index.absolutePath())); proxy->blockedPaths().insert(root.relativeFilePath(index.absolutePath()));
@ -82,43 +93,54 @@ ExportMrPackDialog::ExportMrPackDialog(InstancePtr instance, QWidget* parent)
headerView->setSectionResizeMode(0, QHeaderView::Stretch); headerView->setSectionResizeMode(0, QHeaderView::Stretch);
} }
ExportMrPackDialog::~ExportMrPackDialog() ExportPackDialog::~ExportPackDialog()
{ {
delete ui; delete ui;
} }
void ExportMrPackDialog::done(int result) void ExportPackDialog::done(int result)
{ {
if (result == Accepted) { if (result == Accepted) {
const QString filename = FS::RemoveInvalidFilenameChars(ui->name->text()); const QString filename = FS::RemoveInvalidFilenameChars(ui->name->text());
const QString output = QFileDialog::getSaveFileName(this, tr("Export %1").arg(ui->name->text()), QString output;
FS::PathCombine(QDir::homePath(), filename + ".mrpack"), if (m_provider == ModPlatform::ResourceProvider::MODRINTH)
"Modrinth pack (*.mrpack *.zip)", nullptr); output = QFileDialog::getSaveFileName(this, tr("Export %1").arg(ui->name->text()),
FS::PathCombine(QDir::homePath(), filename + ".mrpack"), "Modrinth pack (*.mrpack *.zip)",
nullptr);
else
output = QFileDialog::getSaveFileName(this, tr("Export %1").arg(ui->name->text()),
FS::PathCombine(QDir::homePath(), filename + ".zip"), "CurseForge pack (*.zip)", nullptr);
if (output.isEmpty()) if (output.isEmpty())
return; return;
Task* task;
if (m_provider == ModPlatform::ResourceProvider::MODRINTH)
task = new ModrinthPackExportTask(ui->name->text(), ui->version->text(), ui->summary->text(), instance, output,
std::bind(&FileIgnoreProxy::filterFile, proxy, std::placeholders::_1));
else
task = new FlamePackExportTask(ui->name->text(), ui->version->text(), ui->summary->text(), instance, output,
std::bind(&FileIgnoreProxy::filterFile, proxy, std::placeholders::_1));
ModrinthPackExportTask task(ui->name->text(), ui->version->text(), ui->summary->text(), instance, output, connect(task, &Task::failed,
[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(); }); [this](const QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); });
connect(&task, &Task::aborted, [this] { connect(task, &Task::aborted, [this] {
CustomMessageBox::selectable(this, tr("Task aborted"), tr("The task has been aborted by the user."), QMessageBox::Information) CustomMessageBox::selectable(this, tr("Task aborted"), tr("The task has been aborted by the user."), QMessageBox::Information)
->show(); ->show();
}); });
connect(task, &Task::finished, [task] { task->deleteLater(); });
ProgressDialog progress(this); ProgressDialog progress(this);
progress.setSkipButton(true, tr("Abort")); progress.setSkipButton(true, tr("Abort"));
if (progress.execWithTask(&task) != QDialog::Accepted) if (progress.execWithTask(task) != QDialog::Accepted)
return; return;
} }
QDialog::done(result); QDialog::done(result);
} }
void ExportMrPackDialog::validate() void ExportPackDialog::validate()
{ {
const bool invalid = ui->name->text().isEmpty() || ui->version->text().isEmpty(); const bool invalid =
ui->name->text().isEmpty() || ((m_provider == ModPlatform::ResourceProvider::MODRINTH) && ui->version->text().isEmpty());
ui->buttonBox->button(QDialogButtonBox::Ok)->setDisabled(invalid); ui->buttonBox->button(QDialogButtonBox::Ok)->setDisabled(invalid);
} }

View File

@ -22,24 +22,28 @@
#include "BaseInstance.h" #include "BaseInstance.h"
#include "FastFileIconProvider.h" #include "FastFileIconProvider.h"
#include "FileIgnoreProxy.h" #include "FileIgnoreProxy.h"
#include "modplatform/ModIndex.h"
namespace Ui { namespace Ui {
class ExportMrPackDialog; class ExportPackDialog;
} }
class ExportMrPackDialog : public QDialog { class ExportPackDialog : public QDialog {
Q_OBJECT Q_OBJECT
public: public:
explicit ExportMrPackDialog(InstancePtr instance, QWidget* parent = nullptr); explicit ExportPackDialog(InstancePtr instance,
~ExportMrPackDialog(); QWidget* parent = nullptr,
ModPlatform::ResourceProvider provider = ModPlatform::ResourceProvider::MODRINTH);
~ExportPackDialog();
void done(int result) override; void done(int result) override;
void validate(); void validate();
private: private:
const InstancePtr instance; const InstancePtr instance;
Ui::ExportMrPackDialog* ui; Ui::ExportPackDialog* ui;
FileIgnoreProxy* proxy; FileIgnoreProxy* proxy;
FastFileIconProvider icons; FastFileIconProvider icons;
const ModPlatform::ResourceProvider m_provider;
}; };

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0"> <ui version="4.0">
<class>ExportMrPackDialog</class> <class>ExportPackDialog</class>
<widget class="QDialog" name="ExportMrPackDialog"> <widget class="QDialog" name="ExportPackDialog">
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>0</x> <x>0</x>
@ -11,7 +11,7 @@
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
<string>Export Modrinth Pack</string> <string>Export Pack</string>
</property> </property>
<property name="sizeGripEnabled"> <property name="sizeGripEnabled">
<bool>true</bool> <bool>true</bool>
@ -24,7 +24,7 @@
</property> </property>
<layout class="QGridLayout" name="gridLayout"> <layout class="QGridLayout" name="gridLayout">
<item row="3" column="0"> <item row="3" column="0">
<widget class="QLabel" name="versionLabel"> <widget class="QLabel" name="summaryLabel">
<property name="text"> <property name="text">
<string>Summary</string> <string>Summary</string>
</property> </property>
@ -41,7 +41,7 @@
</widget> </widget>
</item> </item>
<item row="1" column="0"> <item row="1" column="0">
<widget class="QLabel" name="summaryLabel"> <widget class="QLabel" name="versionLabel">
<property name="text"> <property name="text">
<string>Version</string> <string>Version</string>
</property> </property>
@ -57,6 +57,7 @@
</property> </property>
</widget> </widget>
</item> </item>
</layout> </layout>
</widget> </widget>
</item> </item>
@ -103,7 +104,7 @@
<connection> <connection>
<sender>buttonBox</sender> <sender>buttonBox</sender>
<signal>accepted()</signal> <signal>accepted()</signal>
<receiver>ExportMrPackDialog</receiver> <receiver>ExportPackDialog</receiver>
<slot>accept()</slot> <slot>accept()</slot>
<hints> <hints>
<hint type="sourcelabel"> <hint type="sourcelabel">
@ -119,7 +120,7 @@
<connection> <connection>
<sender>buttonBox</sender> <sender>buttonBox</sender>
<signal>rejected()</signal> <signal>rejected()</signal>
<receiver>ExportMrPackDialog</receiver> <receiver>ExportPackDialog</receiver>
<slot>reject()</slot> <slot>reject()</slot>
<hints> <hints>
<hint type="sourcelabel"> <hint type="sourcelabel">

View File

@ -0,0 +1,223 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "ExportToModListDialog.h"
#include <QCheckBox>
#include <QComboBox>
#include <QTextEdit>
#include "FileSystem.h"
#include "Markdown.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/mod/ModFolderModel.h"
#include "modplatform/helpers/ExportToModList.h"
#include "ui_ExportToModListDialog.h"
#include <QFileDialog>
#include <QFileSystemModel>
#include <QJsonDocument>
#include <QMessageBox>
#include <QPushButton>
const QHash<ExportToModList::Formats, QString> ExportToModListDialog::exampleLines = {
{ ExportToModList::HTML, "<li><a href=\"{url}\">{name}</a> [{version}] by {authors}</li>" },
{ ExportToModList::MARKDOWN, "[{name}]({url}) [{version}] by {authors}" },
{ ExportToModList::PLAINTXT, "{name} ({url}) [{version}] by {authors}" },
{ ExportToModList::JSON, "{\"name\":\"{name}\",\"url\":\"{url}\",\"version\":\"{version}\",\"authors\":\"{authors}\"}," },
{ ExportToModList::CSV, "{name},{url},{version},\"{authors}\"" },
};
ExportToModListDialog::ExportToModListDialog(InstancePtr instance, QWidget* parent)
: QDialog(parent), m_template_changed(false), name(instance->name()), ui(new Ui::ExportToModListDialog)
{
ui->setupUi(this);
enableCustom(false);
MinecraftInstance* mcInstance = dynamic_cast<MinecraftInstance*>(instance.get());
if (mcInstance) {
mcInstance->loaderModList()->update();
connect(mcInstance->loaderModList().get(), &ModFolderModel::updateFinished, this, [this, mcInstance]() {
m_allMods = mcInstance->loaderModList()->allMods();
triggerImp();
});
}
connect(ui->formatComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &ExportToModListDialog::formatChanged);
connect(ui->authorsCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger);
connect(ui->versionCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger);
connect(ui->urlCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger);
connect(ui->authorsButton, &QPushButton::clicked, this, [this](bool) { addExtra(ExportToModList::Authors); });
connect(ui->versionButton, &QPushButton::clicked, this, [this](bool) { addExtra(ExportToModList::Version); });
connect(ui->urlButton, &QPushButton::clicked, this, [this](bool) { addExtra(ExportToModList::Url); });
connect(ui->templateText, &QTextEdit::textChanged, this, [this] {
if (ui->templateText->toPlainText() != exampleLines[format])
ui->formatComboBox->setCurrentIndex(5);
else
triggerImp();
});
connect(ui->copyButton, &QPushButton::clicked, this, [this](bool) {
this->ui->finalText->selectAll();
this->ui->finalText->copy();
});
}
ExportToModListDialog::~ExportToModListDialog()
{
delete ui;
}
void ExportToModListDialog::formatChanged(int index)
{
switch (index) {
case 0: {
enableCustom(false);
ui->resultText->show();
format = ExportToModList::HTML;
break;
}
case 1: {
enableCustom(false);
ui->resultText->show();
format = ExportToModList::MARKDOWN;
break;
}
case 2: {
enableCustom(false);
ui->resultText->hide();
format = ExportToModList::PLAINTXT;
break;
}
case 3: {
enableCustom(false);
ui->resultText->hide();
format = ExportToModList::JSON;
break;
}
case 4: {
enableCustom(false);
ui->resultText->hide();
format = ExportToModList::CSV;
break;
}
case 5: {
m_template_changed = true;
enableCustom(true);
ui->resultText->hide();
format = ExportToModList::CUSTOM;
break;
}
}
triggerImp();
}
void ExportToModListDialog::triggerImp()
{
if (format == ExportToModList::CUSTOM) {
ui->finalText->setPlainText(ExportToModList::exportToModList(m_allMods, ui->templateText->toPlainText()));
return;
}
auto opt = 0;
if (ui->authorsCheckBox->isChecked())
opt |= ExportToModList::Authors;
if (ui->versionCheckBox->isChecked())
opt |= ExportToModList::Version;
if (ui->urlCheckBox->isChecked())
opt |= ExportToModList::Url;
auto txt = ExportToModList::exportToModList(m_allMods, format, static_cast<ExportToModList::OptionalData>(opt));
ui->finalText->setPlainText(txt);
switch (format) {
case ExportToModList::CUSTOM:
return;
case ExportToModList::HTML:
ui->resultText->setHtml(txt);
break;
case ExportToModList::MARKDOWN:
ui->resultText->setHtml(markdownToHTML(txt));
break;
case ExportToModList::PLAINTXT:
break;
case ExportToModList::JSON:
break;
case ExportToModList::CSV:
break;
}
auto exampleLine = exampleLines[format];
if (!m_template_changed && ui->templateText->toPlainText() != exampleLine)
ui->templateText->setPlainText(exampleLine);
}
void ExportToModListDialog::done(int result)
{
if (result == Accepted) {
const QString filename = FS::RemoveInvalidFilenameChars(name);
const QString output =
QFileDialog::getSaveFileName(this, tr("Export %1").arg(name), FS::PathCombine(QDir::homePath(), filename + extension()),
"File (*.txt *.html *.md *.json *.csv)", nullptr);
if (output.isEmpty())
return;
FS::write(output, ui->finalText->toPlainText().toUtf8());
}
QDialog::done(result);
}
QString ExportToModListDialog::extension()
{
switch (format) {
case ExportToModList::HTML:
return ".html";
case ExportToModList::MARKDOWN:
return ".md";
case ExportToModList::PLAINTXT:
return ".txt";
case ExportToModList::CUSTOM:
return ".txt";
case ExportToModList::JSON:
return ".json";
case ExportToModList::CSV:
return ".csv";
}
return ".txt";
}
void ExportToModListDialog::addExtra(ExportToModList::OptionalData option)
{
if (format != ExportToModList::CUSTOM)
return;
switch (option) {
case ExportToModList::Authors:
ui->templateText->insertPlainText("{authors}");
break;
case ExportToModList::Url:
ui->templateText->insertPlainText("{url}");
break;
case ExportToModList::Version:
ui->templateText->insertPlainText("{version}");
break;
}
}
void ExportToModListDialog::enableCustom(bool enabled)
{
ui->authorsCheckBox->setHidden(enabled);
ui->versionCheckBox->setHidden(enabled);
ui->urlCheckBox->setHidden(enabled);
ui->authorsButton->setHidden(!enabled);
ui->versionButton->setHidden(!enabled);
ui->urlButton->setHidden(!enabled);
}

View File

@ -0,0 +1,55 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QDialog>
#include <QList>
#include "BaseInstance.h"
#include "minecraft/mod/Mod.h"
#include "modplatform/helpers/ExportToModList.h"
namespace Ui {
class ExportToModListDialog;
}
class ExportToModListDialog : public QDialog {
Q_OBJECT
public:
explicit ExportToModListDialog(InstancePtr instance, QWidget* parent = nullptr);
~ExportToModListDialog();
void done(int result) override;
protected slots:
void formatChanged(int index);
void triggerImp();
void trigger(int) { triggerImp(); };
void addExtra(ExportToModList::OptionalData option);
private:
QString extension();
void enableCustom(bool enabled);
QList<Mod*> m_allMods;
bool m_template_changed;
QString name;
ExportToModList::Formats format = ExportToModList::Formats::HTML;
Ui::ExportToModListDialog* ui;
static const QHash<ExportToModList::Formats, QString> exampleLines;
};

View File

@ -0,0 +1,240 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ExportToModListDialog</class>
<widget class="QDialog" name="ExportToModListDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>650</width>
<height>446</height>
</rect>
</property>
<property name="windowTitle">
<string>Export Pack to ModList</string>
</property>
<property name="sizeGripEnabled">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,0">
<item>
<widget class="QGroupBox" name="groupBox_3">
<property name="title">
<string>Settings</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="1">
<widget class="QComboBox" name="formatComboBox">
<item>
<property name="text">
<string>HTML</string>
</property>
</item>
<item>
<property name="text">
<string>Markdown</string>
</property>
</item>
<item>
<property name="text">
<string>Plaintext</string>
</property>
</item>
<item>
<property name="text">
<string>JSON</string>
</property>
</item>
<item>
<property name="text">
<string>CSV</string>
</property>
</item>
<item>
<property name="text">
<string>Custom</string>
</property>
</item>
</widget>
</item>
<item row="1" column="0">
<widget class="QGroupBox" name="templateGroup">
<property name="title">
<string>Template</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QTextEdit" name="templateText"/>
</item>
</layout>
</widget>
</item>
<item row="1" column="1">
<widget class="QGroupBox" name="optionsGroup">
<property name="title">
<string>Optional Info</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<widget class="QCheckBox" name="versionCheckBox">
<property name="text">
<string>Version</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="authorsCheckBox">
<property name="text">
<string>Authors</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="urlCheckBox">
<property name="text">
<string>URL</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="versionButton">
<property name="text">
<string>Version</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="authorsButton">
<property name="text">
<string>Authors</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="urlButton">
<property name="text">
<string>URL</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
<property name="lineWidth">
<number>1</number>
</property>
<property name="text">
<string>Format</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_4">
<property name="title">
<string>Result</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPlainTextEdit" name="finalText">
<property name="minimumSize">
<size>
<width>0</width>
<height>143</height>
</size>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QTextBrowser" name="resultText">
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QLabel" name="warningLabel">
<property name="text">
<string>This depends on the mods' metadata. To ensure it is available, run an update on the instance. Installing the updates isn't necessary.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QPushButton" name="copyButton">
<property name="text">
<string>Copy</string>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Save</set>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>ExportToModListDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>334</x>
<y>435</y>
</hint>
<hint type="destinationlabel">
<x>324</x>
<y>206</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>ExportToModListDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>324</x>
<y>390</y>
</hint>
<hint type="destinationlabel">
<x>324</x>
<y>206</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -34,6 +34,7 @@
*/ */
#include "ProgressDialog.h" #include "ProgressDialog.h"
#include <QPoint>
#include "ui_ProgressDialog.h" #include "ui_ProgressDialog.h"
#include <limits> #include <limits>
@ -66,8 +67,9 @@ ProgressDialog::ProgressDialog(QWidget* parent) : QDialog(parent), ui(new Ui::Pr
ui->taskProgressScrollArea->setHidden(true); ui->taskProgressScrollArea->setHidden(true);
this->setWindowFlags(this->windowFlags() & ~Qt::WindowContextHelpButtonHint); this->setWindowFlags(this->windowFlags() & ~Qt::WindowContextHelpButtonHint);
setAttribute(Qt::WidgetAttribute::WA_QuitOnClose, true); setAttribute(Qt::WidgetAttribute::WA_QuitOnClose, true);
setSkipButton(false);
changeProgress(0, 100); changeProgress(0, 100);
updateSize(true);
setSkipButton(false);
} }
void ProgressDialog::setSkipButton(bool present, QString label) void ProgressDialog::setSkipButton(bool present, QString label)
@ -93,24 +95,38 @@ ProgressDialog::~ProgressDialog()
delete ui; delete ui;
} }
void ProgressDialog::updateSize() void ProgressDialog::updateSize(bool recenterParent)
{ {
QSize lastSize = this->size(); QSize lastSize = this->size();
QSize qSize = QSize(480, minimumSizeHint().height()); QPoint lastPos = this->pos();
int minHeight = ui->globalStatusDetailsLabel->minimumSize().height() + (ui->verticalLayout->spacing() * 2);
minHeight += ui->globalProgressBar->minimumSize().height() + ui->verticalLayout->spacing();
if (!ui->taskProgressScrollArea->isHidden())
minHeight += ui->taskProgressScrollArea->minimumSizeHint().height() + ui->verticalLayout->spacing();
if (ui->skipButton->isVisible())
minHeight += ui->skipButton->height() + ui->verticalLayout->spacing();
minHeight = std::max(minHeight, 60);
QSize minSize = QSize(480, minHeight);
// if the current window is too small setMinimumSize(minSize);
if ((lastSize != qSize) && (lastSize.height() < qSize.height())) adjustSize();
{
resize(qSize);
// keep the dialog in the center after a resize QSize newSize = this->size();
this->move( // if the current window is a different size
this->parentWidget()->x() + (this->parentWidget()->width() - this->width()) / 2, auto parent = this->parentWidget();
this->parentWidget()->y() + (this->parentWidget()->height() - this->height()) / 2 if (recenterParent && parent) {
); auto newX = std::max(0, parent->x() + ((parent->width() - newSize.width()) / 2));
auto newY = std::max(0, parent->y() + ((parent->height() - newSize.height()) / 2));
this->move(newX, newY);
}
else if (lastSize != newSize)
{
// center on old position after resize
QSize sizeDiff = lastSize - newSize; // last size was smaller, the results should be negative
auto newX = std::max(0, lastPos.x() + (sizeDiff.width() / 2));
auto newY = std::max(0, lastPos.y() + (sizeDiff.height() / 2));
this->move(newX, newY);
} }
setMinimumSize(qSize);
} }
@ -201,7 +217,9 @@ void ProgressDialog::onTaskSucceeded()
void ProgressDialog::changeStatus(const QString& status) void ProgressDialog::changeStatus(const QString& status)
{ {
ui->globalStatusLabel->setText(task->getStatus()); ui->globalStatusLabel->setText(task->getStatus());
ui->globalStatusLabel->adjustSize();
ui->globalStatusDetailsLabel->setText(task->getDetails()); ui->globalStatusDetailsLabel->setText(task->getDetails());
ui->globalStatusDetailsLabel->adjustSize();
updateSize(); updateSize();
} }

View File

@ -62,7 +62,7 @@ public:
explicit ProgressDialog(QWidget *parent = 0); explicit ProgressDialog(QWidget *parent = 0);
~ProgressDialog(); ~ProgressDialog();
void updateSize(); void updateSize(bool recenterParent = false);
int execWithTask(Task* task); int execWithTask(Task* task);
int execWithTask(std::unique_ptr<Task> &&task); int execWithTask(std::unique_ptr<Task> &&task);

Some files were not shown because too many files have changed in this diff Show More