Merge branch 'develop' of https://github.com/PrismLauncher/PrismLauncher into move_export2

This commit is contained in:
Trial97 2024-04-20 17:42:58 +03:00
commit d88fb266a5
No known key found for this signature in database
GPG Key ID: 55EF5DA53DB36318
122 changed files with 1151 additions and 715 deletions

View File

@ -16,6 +16,7 @@ jobs:
permissions: permissions:
contents: write # for korthout/backport-action to create branch contents: write # for korthout/backport-action to create branch
pull-requests: write # for korthout/backport-action to create PR to backport pull-requests: write # for korthout/backport-action to create PR to backport
actions: write # for korthout/backport-action to create PR with workflow changes
name: Backport Pull Request 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)) 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 runs-on: ubuntu-latest
@ -24,7 +25,7 @@ jobs:
with: with:
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
- name: Create backport PRs - name: Create backport PRs
uses: korthout/backport-action@v2.2.0 uses: korthout/backport-action@v2.5.0
with: with:
# Config README: https://github.com/korthout/backport-action#backport-action # Config README: https://github.com/korthout/backport-action#backport-action
pull_description: |- pull_description: |-

View File

@ -21,8 +21,23 @@ on:
WINDOWS_CODESIGN_PASSWORD: WINDOWS_CODESIGN_PASSWORD:
description: Password for signing Windows builds description: Password for signing Windows builds
required: false required: false
CACHIX_AUTH_TOKEN: APPLE_CODESIGN_CERT:
description: Private token for authenticating against Cachix cache description: Certificate for signing macOS builds
required: false
APPLE_CODESIGN_PASSWORD:
description: Password for signing macOS builds
required: false
APPLE_CODESIGN_ID:
description: Certificate ID for signing macOS builds
required: false
APPLE_NOTARIZE_APPLE_ID:
description: Apple ID used for notarizing macOS builds
required: false
APPLE_NOTARIZE_TEAM_ID:
description: Team ID used for notarizing macOS builds
required: false
APPLE_NOTARIZE_PASSWORD:
description: Password used for notarizing macOS builds
required: false required: false
GPG_PRIVATE_KEY: GPG_PRIVATE_KEY:
description: Private key for AppImage signing description: Private key for AppImage signing
@ -61,7 +76,7 @@ jobs:
qt_ver: 6 qt_ver: 6
qt_host: windows qt_host: windows
qt_arch: '' qt_arch: ''
qt_version: '6.6.1' qt_version: '6.7.0'
qt_modules: 'qt5compat qtimageformats' qt_modules: 'qt5compat qtimageformats'
qt_tools: '' qt_tools: ''
@ -73,7 +88,7 @@ jobs:
qt_ver: 6 qt_ver: 6
qt_host: windows qt_host: windows
qt_arch: 'win64_msvc2019_arm64' qt_arch: 'win64_msvc2019_arm64'
qt_version: '6.6.1' qt_version: '6.7.0'
qt_modules: 'qt5compat qtimageformats' qt_modules: 'qt5compat qtimageformats'
qt_tools: '' qt_tools: ''
@ -83,7 +98,7 @@ jobs:
qt_ver: 6 qt_ver: 6
qt_host: mac qt_host: mac
qt_arch: '' qt_arch: ''
qt_version: '6.6.1' qt_version: '6.7.0'
qt_modules: 'qt5compat qtimageformats' qt_modules: 'qt5compat qtimageformats'
qt_tools: '' qt_tools: ''
@ -145,13 +160,13 @@ jobs:
- name: Setup ccache - name: Setup ccache
if: (runner.os != 'Windows' || matrix.msystem == '') && inputs.build_type == 'Debug' if: (runner.os != 'Windows' || matrix.msystem == '') && inputs.build_type == 'Debug'
uses: hendrikmuhs/ccache-action@v1.2.10 uses: hendrikmuhs/ccache-action@v1.2.12
with: with:
key: ${{ matrix.os }}-qt${{ matrix.qt_ver }}-${{ matrix.architecture }} key: ${{ matrix.os }}-qt${{ matrix.qt_ver }}-${{ matrix.architecture }}
- name: Retrieve ccache cache (Windows MinGW-w64) - name: Retrieve ccache cache (Windows MinGW-w64)
if: runner.os == 'Windows' && matrix.msystem != '' && inputs.build_type == 'Debug' if: runner.os == 'Windows' && matrix.msystem != '' && inputs.build_type == 'Debug'
uses: actions/cache@v3.3.2 uses: actions/cache@v4.0.2
with: with:
path: '${{ github.workspace }}\.ccache' path: '${{ github.workspace }}\.ccache'
key: ${{ matrix.os }}-mingw-w64-ccache-${{ github.run_id }} key: ${{ matrix.os }}-mingw-w64-ccache-${{ github.run_id }}
@ -336,6 +351,20 @@ jobs:
# PACKAGE BUILDS # PACKAGE BUILDS
## ##
- name: Fetch codesign certificate (macOS)
if: runner.os == 'macOS'
run: |
echo '${{ secrets.APPLE_CODESIGN_CERT }}' | base64 --decode > codesign.p12
if [ -n '${{ secrets.APPLE_CODESIGN_ID }}' ]; then
security create-keychain -p '${{ secrets.APPLE_CODESIGN_PASSWORD }}' build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p '${{ secrets.APPLE_CODESIGN_PASSWORD }}' build.keychain
security import codesign.p12 -k build.keychain -P '${{ secrets.APPLE_CODESIGN_PASSWORD }}' -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k '${{ secrets.APPLE_CODESIGN_PASSWORD }}' build.keychain
else
echo ":warning: Using ad-hoc code signing for macOS, as certificate was not present." >> $GITHUB_STEP_SUMMARY
fi
- name: Package (macOS) - name: Package (macOS)
if: runner.os == 'macOS' if: runner.os == 'macOS'
run: | run: |
@ -343,9 +372,34 @@ jobs:
cd ${{ env.INSTALL_DIR }} cd ${{ env.INSTALL_DIR }}
chmod +x "PrismLauncher.app/Contents/MacOS/prismlauncher" chmod +x "PrismLauncher.app/Contents/MacOS/prismlauncher"
sudo codesign --sign - --deep --force --entitlements "../program_info/App.entitlements" --options runtime "PrismLauncher.app/Contents/MacOS/prismlauncher"
if [ -n '${{ secrets.APPLE_CODESIGN_ID }}' ]; then
APPLE_CODESIGN_ID='${{ secrets.APPLE_CODESIGN_ID }}'
else
APPLE_CODESIGN_ID='-'
fi
sudo codesign --sign "$APPLE_CODESIGN_ID" --deep --force --entitlements "../program_info/App.entitlements" --options runtime "PrismLauncher.app/Contents/MacOS/prismlauncher"
mv "PrismLauncher.app" "Prism Launcher.app" mv "PrismLauncher.app" "Prism Launcher.app"
tar -czf ../PrismLauncher.tar.gz *
- name: Notarize (macOS)
if: runner.os == 'macOS'
run: |
cd ${{ env.INSTALL_DIR }}
if [ -n '${{ secrets.APPLE_NOTARIZE_PASSWORD }}' ]; then
ditto -c -k --sequesterRsrc --keepParent "Prism Launcher.app" ../PrismLauncher.zip
xcrun notarytool submit ../PrismLauncher.zip \
--wait --progress \
--apple-id '${{ secrets.APPLE_NOTARIZE_APPLE_ID }}' \
--team-id '${{ secrets.APPLE_NOTARIZE_TEAM_ID }}' \
--password '${{ secrets.APPLE_NOTARIZE_PASSWORD }}'
xcrun stapler staple "Prism Launcher.app"
else
echo ":warning: Skipping notarization as credentials are not present." >> $GITHUB_STEP_SUMMARY
fi
ditto -c -k --sequesterRsrc --keepParent "Prism Launcher.app" ../PrismLauncher.zip
- name: Make Sparkle signature (macOS) - name: Make Sparkle signature (macOS)
if: matrix.name == 'macOS' if: matrix.name == 'macOS'
@ -353,7 +407,7 @@ jobs:
if [ '${{ secrets.SPARKLE_ED25519_KEY }}' != '' ]; then if [ '${{ secrets.SPARKLE_ED25519_KEY }}' != '' ]; then
brew install openssl@3 brew install openssl@3
echo '${{ secrets.SPARKLE_ED25519_KEY }}' > ed25519-priv.pem echo '${{ secrets.SPARKLE_ED25519_KEY }}' > ed25519-priv.pem
signature=$(/usr/local/opt/openssl@3/bin/openssl pkeyutl -sign -rawin -in ${{ github.workspace }}/PrismLauncher.tar.gz -inkey ed25519-priv.pem | openssl base64 | tr -d \\n) signature=$(/usr/local/opt/openssl@3/bin/openssl pkeyutl -sign -rawin -in ${{ github.workspace }}/PrismLauncher.zip -inkey ed25519-priv.pem | openssl base64 | tr -d \\n)
rm ed25519-priv.pem rm ed25519-priv.pem
cat >> $GITHUB_STEP_SUMMARY << EOF cat >> $GITHUB_STEP_SUMMARY << EOF
### Artifact Information :information_source: ### Artifact Information :information_source:
@ -442,7 +496,6 @@ jobs:
run: | run: |
cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_DIR }} cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_DIR }}
for l in $(find ${{ env.INSTALL_DIR }} -type f); do l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_DIR }}/}; l=${l#./}; echo $l; done > ${{ env.INSTALL_DIR }}/manifest.txt for l in $(find ${{ env.INSTALL_DIR }} -type f); do l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_DIR }}/}; l=${l#./}; echo $l; done > ${{ env.INSTALL_DIR }}/manifest.txt
cd ${{ env.INSTALL_DIR }} cd ${{ env.INSTALL_DIR }}
tar --owner root --group root -czf ../PrismLauncher.tar.gz * tar --owner root --group root -czf ../PrismLauncher.tar.gz *
@ -451,9 +504,12 @@ jobs:
run: | run: |
cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }}
cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable
# workaround to make portable installs to work on fedora
mkdir ${{ env.INSTALL_PORTABLE_DIR }}/lib
cp /lib/x86_64-linux-gnu/libbz2.so.1.0 ${{ env.INSTALL_PORTABLE_DIR }}/lib
for l in $(find ${{ env.INSTALL_PORTABLE_DIR }} -type f); do l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_PORTABLE_DIR }}/}; l=${l#./}; echo $l; done > ${{ env.INSTALL_PORTABLE_DIR }}/manifest.txt for l in $(find ${{ env.INSTALL_PORTABLE_DIR }} -type f); do l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_PORTABLE_DIR }}/}; l=${l#./}; echo $l; done > ${{ env.INSTALL_PORTABLE_DIR }}/manifest.txt
cd ${{ env.INSTALL_PORTABLE_DIR }} cd ${{ env.INSTALL_PORTABLE_DIR }}
tar -czf ../PrismLauncher-portable.tar.gz * tar -czf ../PrismLauncher-portable.tar.gz *
@ -517,70 +573,70 @@ jobs:
- name: Upload binary tarball (macOS) - name: Upload binary tarball (macOS)
if: runner.os == 'macOS' if: runner.os == 'macOS'
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: PrismLauncher-${{ matrix.name }}-${{ env.VERSION }}-${{ inputs.build_type }} name: PrismLauncher-${{ matrix.name }}-${{ env.VERSION }}-${{ inputs.build_type }}
path: PrismLauncher.tar.gz path: PrismLauncher.zip
- name: Upload binary zip (Windows) - name: Upload binary zip (Windows)
if: runner.os == 'Windows' if: runner.os == 'Windows'
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: PrismLauncher-${{ matrix.name }}-${{ env.VERSION }}-${{ inputs.build_type }} name: PrismLauncher-${{ matrix.name }}-${{ env.VERSION }}-${{ inputs.build_type }}
path: ${{ env.INSTALL_DIR }}/** path: ${{ env.INSTALL_DIR }}/**
- name: Upload binary zip (Windows, portable) - name: Upload binary zip (Windows, portable)
if: runner.os == 'Windows' if: runner.os == 'Windows'
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: PrismLauncher-${{ matrix.name }}-Portable-${{ env.VERSION }}-${{ inputs.build_type }} name: PrismLauncher-${{ matrix.name }}-Portable-${{ env.VERSION }}-${{ inputs.build_type }}
path: ${{ env.INSTALL_PORTABLE_DIR }}/** path: ${{ env.INSTALL_PORTABLE_DIR }}/**
- name: Upload installer (Windows) - name: Upload installer (Windows)
if: runner.os == 'Windows' if: runner.os == 'Windows'
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: PrismLauncher-${{ matrix.name }}-Setup-${{ env.VERSION }}-${{ inputs.build_type }} name: PrismLauncher-${{ matrix.name }}-Setup-${{ env.VERSION }}-${{ inputs.build_type }}
path: PrismLauncher-Setup.exe path: PrismLauncher-Setup.exe
- name: Upload binary tarball (Linux, Qt 5) - name: Upload binary tarball (Linux, Qt 5)
if: runner.os == 'Linux' && matrix.qt_ver != 6 if: runner.os == 'Linux' && matrix.qt_ver != 6
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: PrismLauncher-${{ runner.os }}-Qt5-${{ env.VERSION }}-${{ inputs.build_type }} name: PrismLauncher-${{ runner.os }}-Qt5-${{ env.VERSION }}-${{ inputs.build_type }}
path: PrismLauncher.tar.gz path: PrismLauncher.tar.gz
- name: Upload binary tarball (Linux, portable, Qt 5) - name: Upload binary tarball (Linux, portable, Qt 5)
if: runner.os == 'Linux' && matrix.qt_ver != 6 if: runner.os == 'Linux' && matrix.qt_ver != 6
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: PrismLauncher-${{ runner.os }}-Qt5-Portable-${{ env.VERSION }}-${{ inputs.build_type }} name: PrismLauncher-${{ runner.os }}-Qt5-Portable-${{ env.VERSION }}-${{ inputs.build_type }}
path: PrismLauncher-portable.tar.gz path: PrismLauncher-portable.tar.gz
- name: Upload binary tarball (Linux, Qt 6) - name: Upload binary tarball (Linux, Qt 6)
if: runner.os == 'Linux' && matrix.qt_ver !=5 if: runner.os == 'Linux' && matrix.qt_ver !=5
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: PrismLauncher-${{ runner.os }}-Qt6-${{ env.VERSION }}-${{ inputs.build_type }} name: PrismLauncher-${{ runner.os }}-Qt6-${{ env.VERSION }}-${{ inputs.build_type }}
path: PrismLauncher.tar.gz path: PrismLauncher.tar.gz
- name: Upload binary tarball (Linux, portable, Qt 6) - name: Upload binary tarball (Linux, portable, Qt 6)
if: runner.os == 'Linux' && matrix.qt_ver != 5 if: runner.os == 'Linux' && matrix.qt_ver != 5
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: PrismLauncher-${{ runner.os }}-Qt6-Portable-${{ env.VERSION }}-${{ inputs.build_type }} name: PrismLauncher-${{ runner.os }}-Qt6-Portable-${{ env.VERSION }}-${{ inputs.build_type }}
path: PrismLauncher-portable.tar.gz path: PrismLauncher-portable.tar.gz
- name: Upload AppImage (Linux) - name: Upload AppImage (Linux)
if: runner.os == 'Linux' && matrix.qt_ver != 5 if: runner.os == 'Linux' && matrix.qt_ver != 5
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage name: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage
path: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage path: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage
- name: Upload AppImage Zsync (Linux) - name: Upload AppImage Zsync (Linux)
if: runner.os == 'Linux' && matrix.qt_ver != 5 if: runner.os == 'Linux' && matrix.qt_ver != 5
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage.zsync name: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage.zsync
path: PrismLauncher-Linux-x86_64.AppImage.zsync path: PrismLauncher-Linux-x86_64.AppImage.zsync

View File

@ -13,7 +13,7 @@ jobs:
submodules: 'true' submodules: 'true'
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v2 uses: github/codeql-action/init@v3
with: with:
config-file: ./.github/codeql/codeql-config.yml config-file: ./.github/codeql/codeql-config.yml
queries: security-and-quality queries: security-and-quality
@ -32,4 +32,4 @@ jobs:
cmake --build build cmake --build build
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2 uses: github/codeql-action/analyze@v3

View File

@ -32,6 +32,11 @@ jobs:
SPARKLE_ED25519_KEY: ${{ secrets.SPARKLE_ED25519_KEY }} SPARKLE_ED25519_KEY: ${{ secrets.SPARKLE_ED25519_KEY }}
WINDOWS_CODESIGN_CERT: ${{ secrets.WINDOWS_CODESIGN_CERT }} WINDOWS_CODESIGN_CERT: ${{ secrets.WINDOWS_CODESIGN_CERT }}
WINDOWS_CODESIGN_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }} WINDOWS_CODESIGN_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }}
CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }} APPLE_CODESIGN_CERT: ${{ secrets.APPLE_CODESIGN_CERT }}
APPLE_CODESIGN_PASSWORD: ${{ secrets.APPLE_CODESIGN_PASSWORD }}
APPLE_CODESIGN_ID: ${{ secrets.APPLE_CODESIGN_ID }}
APPLE_NOTARIZE_APPLE_ID: ${{ secrets.APPLE_NOTARIZE_APPLE_ID }}
APPLE_NOTARIZE_TEAM_ID: ${{ secrets.APPLE_NOTARIZE_TEAM_ID }}
APPLE_NOTARIZE_PASSWORD: ${{ secrets.APPLE_NOTARIZE_PASSWORD }}
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
GPG_PRIVATE_KEY_ID: ${{ secrets.GPG_PRIVATE_KEY_ID }} GPG_PRIVATE_KEY_ID: ${{ secrets.GPG_PRIVATE_KEY_ID }}

View File

@ -16,7 +16,12 @@ jobs:
SPARKLE_ED25519_KEY: ${{ secrets.SPARKLE_ED25519_KEY }} SPARKLE_ED25519_KEY: ${{ secrets.SPARKLE_ED25519_KEY }}
WINDOWS_CODESIGN_CERT: ${{ secrets.WINDOWS_CODESIGN_CERT }} WINDOWS_CODESIGN_CERT: ${{ secrets.WINDOWS_CODESIGN_CERT }}
WINDOWS_CODESIGN_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }} WINDOWS_CODESIGN_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }}
CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }} APPLE_CODESIGN_CERT: ${{ secrets.APPLE_CODESIGN_CERT }}
APPLE_CODESIGN_PASSWORD: ${{ secrets.APPLE_CODESIGN_PASSWORD }}
APPLE_CODESIGN_ID: ${{ secrets.APPLE_CODESIGN_ID }}
APPLE_NOTARIZE_APPLE_ID: ${{ secrets.APPLE_NOTARIZE_APPLE_ID }}
APPLE_NOTARIZE_TEAM_ID: ${{ secrets.APPLE_NOTARIZE_TEAM_ID }}
APPLE_NOTARIZE_PASSWORD: ${{ secrets.APPLE_NOTARIZE_PASSWORD }}
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
GPG_PRIVATE_KEY_ID: ${{ secrets.GPG_PRIVATE_KEY_ID }} GPG_PRIVATE_KEY_ID: ${{ secrets.GPG_PRIVATE_KEY_ID }}
@ -32,7 +37,7 @@ jobs:
submodules: "true" submodules: "true"
path: "PrismLauncher-source" path: "PrismLauncher-source"
- name: Download artifacts - name: Download artifacts
uses: actions/download-artifact@v3 uses: actions/download-artifact@v4
- name: Grab and store version - name: Grab and store version
run: | run: |
tag_name=$(echo ${{ github.ref }} | grep -oE "[^/]+$") tag_name=$(echo ${{ github.ref }} | grep -oE "[^/]+$")
@ -46,8 +51,8 @@ jobs:
mv PrismLauncher-Linux-Qt5*/PrismLauncher.tar.gz PrismLauncher-Linux-Qt5-${{ env.VERSION }}.tar.gz mv PrismLauncher-Linux-Qt5*/PrismLauncher.tar.gz PrismLauncher-Linux-Qt5-${{ env.VERSION }}.tar.gz
mv PrismLauncher-*.AppImage/PrismLauncher-*.AppImage PrismLauncher-Linux-x86_64.AppImage mv PrismLauncher-*.AppImage/PrismLauncher-*.AppImage PrismLauncher-Linux-x86_64.AppImage
mv PrismLauncher-*.AppImage.zsync/PrismLauncher-*.AppImage.zsync PrismLauncher-Linux-x86_64.AppImage.zsync mv PrismLauncher-*.AppImage.zsync/PrismLauncher-*.AppImage.zsync PrismLauncher-Linux-x86_64.AppImage.zsync
mv PrismLauncher-macOS-Legacy*/PrismLauncher.tar.gz PrismLauncher-macOS-Legacy-${{ env.VERSION }}.tar.gz mv PrismLauncher-macOS-Legacy*/PrismLauncher.zip PrismLauncher-macOS-Legacy-${{ env.VERSION }}.zip
mv PrismLauncher-macOS*/PrismLauncher.tar.gz PrismLauncher-macOS-${{ env.VERSION }}.tar.gz mv PrismLauncher-macOS*/PrismLauncher.zip PrismLauncher-macOS-${{ env.VERSION }}.zip
tar --exclude='.git' -czf PrismLauncher-${{ env.VERSION }}.tar.gz PrismLauncher-${{ env.VERSION }} tar --exclude='.git' -czf PrismLauncher-${{ env.VERSION }}.tar.gz PrismLauncher-${{ env.VERSION }}
@ -79,7 +84,7 @@ jobs:
- name: Create release - name: Create release
id: create_release id: create_release
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v2
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
tag_name: ${{ github.ref }} tag_name: ${{ github.ref }}
@ -102,6 +107,6 @@ jobs:
PrismLauncher-Windows-MSVC-${{ env.VERSION }}.zip PrismLauncher-Windows-MSVC-${{ env.VERSION }}.zip
PrismLauncher-Windows-MSVC-Portable-${{ env.VERSION }}.zip PrismLauncher-Windows-MSVC-Portable-${{ env.VERSION }}.zip
PrismLauncher-Windows-MSVC-Setup-${{ env.VERSION }}.exe PrismLauncher-Windows-MSVC-Setup-${{ env.VERSION }}.exe
PrismLauncher-macOS-${{ env.VERSION }}.tar.gz PrismLauncher-macOS-${{ env.VERSION }}.zip
PrismLauncher-macOS-Legacy-${{ env.VERSION }}.tar.gz PrismLauncher-macOS-Legacy-${{ env.VERSION }}.zip
PrismLauncher-${{ env.VERSION }}.tar.gz PrismLauncher-${{ env.VERSION }}.tar.gz

View File

@ -17,9 +17,9 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: cachix/install-nix-action@7ac1ec25491415c381d9b62f0657c7a028df52a7 # v24 - uses: cachix/install-nix-action@8887e596b4ee1134dae06b98d573bd674693f47c # v26
- uses: DeterminateSystems/update-flake-lock@v20 - uses: DeterminateSystems/update-flake-lock@v21
with: with:
commit-msg: "chore(nix): update lockfile" commit-msg: "chore(nix): update lockfile"
pr-title: "chore(nix): update lockfile" pr-title: "chore(nix): update lockfile"

View File

@ -178,7 +178,7 @@ set(Launcher_NEWS_OPEN_URL "https://prismlauncher.org/news" CACHE STRING "URL th
set(Launcher_HELP_URL "https://prismlauncher.org/wiki/help-pages/%1" CACHE STRING "URL (with arg %1 to be substituted with page-id) that gets opened when the user requests help") set(Launcher_HELP_URL "https://prismlauncher.org/wiki/help-pages/%1" CACHE STRING "URL (with arg %1 to be substituted with page-id) that gets opened when the user requests help")
######## Set version numbers ######## ######## Set version numbers ########
set(Launcher_VERSION_MAJOR 8) set(Launcher_VERSION_MAJOR 9)
set(Launcher_VERSION_MINOR 0) set(Launcher_VERSION_MINOR 0)
set(Launcher_VERSION_NAME "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}") set(Launcher_VERSION_NAME "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}")
@ -377,12 +377,12 @@ if(UNIX AND APPLE)
set(MACOSX_BUNDLE_SHORT_VERSION_STRING "${Launcher_VERSION_NAME}") set(MACOSX_BUNDLE_SHORT_VERSION_STRING "${Launcher_VERSION_NAME}")
set(MACOSX_BUNDLE_LONG_VERSION_STRING "${Launcher_VERSION_NAME}") set(MACOSX_BUNDLE_LONG_VERSION_STRING "${Launcher_VERSION_NAME}")
set(MACOSX_BUNDLE_ICON_FILE ${Launcher_Name}.icns) set(MACOSX_BUNDLE_ICON_FILE ${Launcher_Name}.icns)
set(MACOSX_BUNDLE_COPYRIGHT "© 2022-2023 ${Launcher_Copyright_Mac}") set(MACOSX_BUNDLE_COPYRIGHT "${Launcher_Copyright_Mac}")
set(MACOSX_SPARKLE_UPDATE_PUBLIC_KEY "v55ZWWD6QlPoXGV6VLzOTZxZUggWeE51X8cRQyQh6vA=" CACHE STRING "Public key for Sparkle update feed") set(MACOSX_SPARKLE_UPDATE_PUBLIC_KEY "v55ZWWD6QlPoXGV6VLzOTZxZUggWeE51X8cRQyQh6vA=" CACHE STRING "Public key for Sparkle update feed")
set(MACOSX_SPARKLE_UPDATE_FEED_URL "https://prismlauncher.org/feed/appcast.xml" CACHE STRING "URL for Sparkle update feed") set(MACOSX_SPARKLE_UPDATE_FEED_URL "https://prismlauncher.org/feed/appcast.xml" CACHE STRING "URL for Sparkle update feed")
set(MACOSX_SPARKLE_DOWNLOAD_URL "https://github.com/sparkle-project/Sparkle/releases/download/2.1.0/Sparkle-2.1.0.tar.xz" CACHE STRING "URL to Sparkle release archive") set(MACOSX_SPARKLE_DOWNLOAD_URL "https://github.com/sparkle-project/Sparkle/releases/download/2.5.2/Sparkle-2.5.2.tar.xz" CACHE STRING "URL to Sparkle release archive")
set(MACOSX_SPARKLE_SHA256 "bf6ac1caa9f8d321d5784859c88da874f28412f37fb327bc21b7b14c5d61ef94" CACHE STRING "SHA256 checksum for Sparkle release archive") set(MACOSX_SPARKLE_SHA256 "572dd67ae398a466f19f343a449e1890bac1ef74885b4739f68f979a8a89884b" CACHE STRING "SHA256 checksum for Sparkle release archive")
set(MACOSX_SPARKLE_DIR "${CMAKE_BINARY_DIR}/frameworks/Sparkle") set(MACOSX_SPARKLE_DIR "${CMAKE_BINARY_DIR}/frameworks/Sparkle")
# directories to look for dependencies # directories to look for dependencies
@ -504,11 +504,10 @@ else()
endif() endif()
if(NOT cmark_FOUND) if(NOT cmark_FOUND)
message(STATUS "Using bundled cmark") message(STATUS "Using bundled cmark")
set(CMARK_STATIC ON CACHE BOOL "Build static libcmark library" FORCE) set(BUILD_TESTING 0)
set(CMARK_SHARED OFF CACHE BOOL "Build shared libcmark library" FORCE) set(BUILD_SHARED_LIBS 0)
set(CMARK_TESTS OFF CACHE BOOL "Build cmark tests and enable testing" FORCE)
add_subdirectory(libraries/cmark EXCLUDE_FROM_ALL) # Markdown parser add_subdirectory(libraries/cmark EXCLUDE_FROM_ALL) # Markdown parser
add_library(cmark::cmark ALIAS cmark_static) add_library(cmark::cmark ALIAS cmark)
else() else()
message(STATUS "Using system cmark") message(STATUS "Using system cmark")
endif() endif()

View File

@ -1,7 +1,7 @@
## Prism Launcher ## Prism Launcher
Prism Launcher - Minecraft Launcher Prism Launcher - Minecraft Launcher
Copyright (C) 2022-2023 Prism Launcher Contributors Copyright (C) 2022-2024 Prism Launcher Contributors
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
@ -436,7 +436,7 @@
Copyright (C) 2007 Johann Ollivier Lapeyre <johann@oxygen-icons.org> Copyright (C) 2007 Johann Ollivier Lapeyre <johann@oxygen-icons.org>
Copyright (C) 2007 Kenneth Wimer <kwwii@bootsplash.org> Copyright (C) 2007 Kenneth Wimer <kwwii@bootsplash.org>
Copyright (C) 2007 Riccardo Iaconelli <riccardo@oxygen-icons.org> Copyright (C) 2007 Riccardo Iaconelli <riccardo@oxygen-icons.org>
and others and others
This library is free software; you can redistribute it and/or This library is free software; you can redistribute it and/or

68
flake.lock generated
View File

@ -18,14 +18,16 @@
}, },
"flake-parts": { "flake-parts": {
"inputs": { "inputs": {
"nixpkgs-lib": "nixpkgs-lib" "nixpkgs-lib": [
"nixpkgs"
]
}, },
"locked": { "locked": {
"lastModified": 1701473968, "lastModified": 1712014858,
"narHash": "sha256-YcVE5emp1qQ8ieHUnxt1wCZCC3ZfAS+SRRWZ2TMda7E=", "narHash": "sha256-sB4SWl2lX95bExY2gMFG5HIzvva5AVMJd4Igm+GpZNw=",
"owner": "hercules-ci", "owner": "hercules-ci",
"repo": "flake-parts", "repo": "flake-parts",
"rev": "34fed993f1674c8d06d58b37ce1e0fe5eebcb9f5", "rev": "9126214d0a59633752a136528f5f3b9aa8565b7d",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -39,11 +41,11 @@
"systems": "systems" "systems": "systems"
}, },
"locked": { "locked": {
"lastModified": 1685518550, "lastModified": 1710146030,
"narHash": "sha256-o2d0KcvaXzTrPRIo0kOLV0/QXHhDQ5DTi+OxcjO8xqY=", "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "flake-utils",
"rev": "a1720a10a6cfe8234c0e93907ffe81be440f4cef", "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -60,11 +62,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1660459072, "lastModified": 1709087332,
"narHash": "sha256-8DFJjXG8zqoONA1vXtgeKXy68KdJL5UaXR8NtVMUbx8=", "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
"owner": "hercules-ci", "owner": "hercules-ci",
"repo": "gitignore.nix", "repo": "gitignore.nix",
"rev": "a20de23b925fd8264fd7fad6454652e142fd7f73", "rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -89,28 +91,13 @@
"type": "github" "type": "github"
} }
}, },
"nix-filter": {
"locked": {
"lastModified": 1701697642,
"narHash": "sha256-L217WytWZHSY8GW9Gx1A64OnNctbuDbfslaTEofXXRw=",
"owner": "numtide",
"repo": "nix-filter",
"rev": "c843418ecfd0344ecb85844b082ff5675e02c443",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "nix-filter",
"type": "github"
}
},
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1701998057, "lastModified": 1712883908,
"narHash": "sha256-gAJGhcTO9cso7XDfAScXUlPcva427AUT2q02qrmXPdo=", "narHash": "sha256-icE1IJE9fHcbDfJ0+qWoDdcBXUoZCcIJxME4lMHwvSM=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "09dc04054ba2ff1f861357d0e7e76d021b273cd7", "rev": "a0c9e3aee1000ac2bfb0e5b98c94c946a5d180a9",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -120,24 +107,6 @@
"type": "github" "type": "github"
} }
}, },
"nixpkgs-lib": {
"locked": {
"dir": "lib",
"lastModified": 1701253981,
"narHash": "sha256-ztaDIyZ7HrTAfEEUt9AtTDNoCYxUdSd6NrRHaYOIxtk=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e92039b55bcd58469325ded85d4f58dd5a4eaf58",
"type": "github"
},
"original": {
"dir": "lib",
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"pre-commit-hooks": { "pre-commit-hooks": {
"inputs": { "inputs": {
"flake-compat": [ "flake-compat": [
@ -153,11 +122,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1700922917, "lastModified": 1712897695,
"narHash": "sha256-ej2fch/T584b5K9sk1UhmZF7W6wEfDHuoUYpFN8dtvM=", "narHash": "sha256-nMirxrGteNAl9sWiOhoN5tIHyjBbVi5e2tgZUgZlK3Y=",
"owner": "cachix", "owner": "cachix",
"repo": "pre-commit-hooks.nix", "repo": "pre-commit-hooks.nix",
"rev": "e5ee5c5f3844550c01d2131096c7271cec5e9b78", "rev": "40e6053ecb65fcbf12863338a6dcefb3f55f1bf8",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -171,7 +140,6 @@
"flake-compat": "flake-compat", "flake-compat": "flake-compat",
"flake-parts": "flake-parts", "flake-parts": "flake-parts",
"libnbtplusplus": "libnbtplusplus", "libnbtplusplus": "libnbtplusplus",
"nix-filter": "nix-filter",
"nixpkgs": "nixpkgs", "nixpkgs": "nixpkgs",
"pre-commit-hooks": "pre-commit-hooks" "pre-commit-hooks": "pre-commit-hooks"
} }

View File

@ -1,15 +1,24 @@
{ {
description = "A custom launcher for Minecraft that allows you to easily manage multiple installations of Minecraft at once (Fork of MultiMC)"; description = "A custom launcher for Minecraft that allows you to easily manage multiple installations of Minecraft at once (Fork of MultiMC)";
nixConfig = {
extra-substituters = ["https://cache.garnix.io"];
extra-trusted-public-keys = ["cache.garnix.io:CTFPyKSLcx5RMJKfLo5EEPUObbA78b0YQ2DTCJXqr9g="];
};
inputs = { inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
flake-parts.url = "github:hercules-ci/flake-parts"; flake-parts = {
nix-filter.url = "github:numtide/nix-filter"; url = "github:hercules-ci/flake-parts";
inputs.nixpkgs-lib.follows = "nixpkgs";
};
pre-commit-hooks = { pre-commit-hooks = {
url = "github:cachix/pre-commit-hooks.nix"; url = "github:cachix/pre-commit-hooks.nix";
inputs.nixpkgs.follows = "nixpkgs"; inputs = {
inputs.nixpkgs-stable.follows = "nixpkgs"; nixpkgs.follows = "nixpkgs";
inputs.flake-compat.follows = "flake-compat"; nixpkgs-stable.follows = "nixpkgs";
flake-compat.follows = "flake-compat";
};
}; };
flake-compat = { flake-compat = {
url = "github:edolstra/flake-compat"; url = "github:edolstra/flake-compat";

View File

@ -1,8 +1,9 @@
id: org.prismlauncher.PrismLauncher id: org.prismlauncher.PrismLauncher
runtime: org.kde.Platform runtime: org.kde.Platform
runtime-version: "5.15-23.08" runtime-version: 5.15-23.08
sdk: org.kde.Sdk sdk: org.kde.Sdk
sdk-extensions: sdk-extensions:
- org.freedesktop.Sdk.Extension.openjdk21
- org.freedesktop.Sdk.Extension.openjdk17 - org.freedesktop.Sdk.Extension.openjdk17
- org.freedesktop.Sdk.Extension.openjdk8 - org.freedesktop.Sdk.Extension.openjdk8
@ -50,6 +51,8 @@ modules:
buildsystem: simple buildsystem: simple
build-commands: build-commands:
- mkdir -p /app/jdk/ - mkdir -p /app/jdk/
- /usr/lib/sdk/openjdk21/install.sh
- mv /app/jre /app/jdk/21
- /usr/lib/sdk/openjdk17/install.sh - /usr/lib/sdk/openjdk17/install.sh
- mv /app/jre /app/jdk/17 - mv /app/jre /app/jdk/17
- /usr/lib/sdk/openjdk8/install.sh - /usr/lib/sdk/openjdk8/install.sh
@ -104,18 +107,15 @@ modules:
- install -Dm755 ../data/gamemoderun -t /app/bin - install -Dm755 ../data/gamemoderun -t /app/bin
sources: sources:
- type: archive - type: archive
archive-type: tar-gzip dest-filename: gamemode.tar.gz
url: https://api.github.com/repos/FeralInteractive/gamemode/tarball/1.7 url: https://api.github.com/repos/FeralInteractive/gamemode/tarball/1.8.1
sha256: 57ce73ba605d1cf12f8d13725006a895182308d93eba0f69f285648449641803 sha256: 969cf85b5ca3944f3e315cd73a0ee9bea4f9c968cd7d485e9f4745bc1e679c4e
x-checker-data: x-checker-data:
type: json type: json
url: https://api.github.com/repos/FeralInteractive/gamemode/releases/latest url: https://api.github.com/repos/FeralInteractive/gamemode/releases/latest
version-query: .tag_name version-query: .tag_name
url-query: .tarball_url url-query: .tarball_url
timestamp-query: .published_at timestamp-query: .published_at
# from https://github.com/flathub/net.gaijin.WarThunder/blob/7ea6f7a9f84b9c77150c003a7059dc03f8dcbc7f/gamemode.patch
- type: patch
path: patches/gamemode.patch
cleanup: cleanup:
- /include - /include
- /lib/pkgconfig - /lib/pkgconfig

View File

@ -1,12 +0,0 @@
diff -ruN a/common/common-pidfds.c b/common/common-pidfds.c
--- a/common/common-pidfds.c 2021-02-18 20:00:12.000000000 +0100
+++ b/common/common-pidfds.c 2023-09-07 08:57:42.954362763 +0200
@@ -58,6 +58,8 @@
{
return (int)syscall(__NR_pidfd_open, pid, flags);
}
+#else
+#include <sys/pidfd.h>
#endif
/* pidfd functions */

@ -1 +1 @@
Subproject commit 45094ca570be383d06df729b6972830ec63bd3df Subproject commit f2b0c16a2a217a1822ce5a6538ba8f755ed1dd32

View File

@ -225,6 +225,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
// Don't quit on hiding the last window // Don't quit on hiding the last window
this->setQuitOnLastWindowClosed(false); this->setQuitOnLastWindowClosed(false);
this->setQuitLockEnabled(false);
// Commandline parsing // Commandline parsing
QCommandLineParser parser; QCommandLineParser parser;
@ -308,7 +309,11 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
adjustedBy = "Persistent data path"; adjustedBy = "Persistent data path";
#ifndef Q_OS_MACOS #ifndef Q_OS_MACOS
if (QFile::exists(FS::PathCombine(m_rootPath, "portable.txt"))) { if (auto portableUserData = FS::PathCombine(m_rootPath, "UserData"); QDir(portableUserData).exists()) {
dataPath = portableUserData;
adjustedBy = "Portable user data path";
m_portable = true;
} else if (QFile::exists(FS::PathCombine(m_rootPath, "portable.txt"))) {
dataPath = m_rootPath; dataPath = m_rootPath;
adjustedBy = "Portable data path"; adjustedBy = "Portable data path";
m_portable = true; m_portable = true;
@ -494,8 +499,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
} }
{ {
qDebug() << qPrintable(BuildConfig.LAUNCHER_DISPLAYNAME) << ", (c) 2022-2023 " qDebug() << qPrintable(BuildConfig.LAUNCHER_DISPLAYNAME + ", " + QString(BuildConfig.LAUNCHER_COPYRIGHT).replace("\n", ", "));
<< qPrintable(QString(BuildConfig.LAUNCHER_COPYRIGHT).replace("\n", ", "));
qDebug() << "Version : " << BuildConfig.printableVersionString(); qDebug() << "Version : " << BuildConfig.printableVersionString();
qDebug() << "Platform : " << BuildConfig.BUILD_PLATFORM; qDebug() << "Platform : " << BuildConfig.BUILD_PLATFORM;
qDebug() << "Git commit : " << BuildConfig.GIT_COMMIT; qDebug() << "Git commit : " << BuildConfig.GIT_COMMIT;
@ -640,10 +644,11 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_settings->registerSetting("UseNativeGLFW", false); m_settings->registerSetting("UseNativeGLFW", false);
m_settings->registerSetting("CustomGLFWPath", ""); m_settings->registerSetting("CustomGLFWPath", "");
// Peformance related options // Performance related options
m_settings->registerSetting("EnableFeralGamemode", false); m_settings->registerSetting("EnableFeralGamemode", false);
m_settings->registerSetting("EnableMangoHud", false); m_settings->registerSetting("EnableMangoHud", false);
m_settings->registerSetting("UseDiscreteGpu", false); m_settings->registerSetting("UseDiscreteGpu", false);
m_settings->registerSetting("UseZink", false);
// Game time // Game time
m_settings->registerSetting("ShowGameTime", true); m_settings->registerSetting("ShowGameTime", true);
@ -667,6 +672,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
// The cat // The cat
m_settings->registerSetting("TheCat", false); m_settings->registerSetting("TheCat", false);
m_settings->registerSetting("CatOpacity", 100);
m_settings->registerSetting("StatusBarVisible", true); m_settings->registerSetting("StatusBarVisible", true);
@ -751,6 +757,9 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_settings->registerSetting("ModrinthToken", ""); m_settings->registerSetting("ModrinthToken", "");
m_settings->registerSetting("UserAgentOverride", ""); m_settings->registerSetting("UserAgentOverride", "");
// FTBApp instances
m_settings->registerSetting("FTBAppInstancesPath", "");
// Init page provider // Init page provider
{ {
m_globalSettingsProvider = std::make_shared<GenericPageProvider>(tr("Settings")); m_globalSettingsProvider = std::make_shared<GenericPageProvider>(tr("Settings"));

View File

@ -37,140 +37,33 @@
#include <QDesktopServices> #include <QDesktopServices>
#include <QDir> #include <QDir>
#include <QProcess> #include <QProcess>
#include "FileSystem.h"
/**
* This shouldn't exist, but until QTBUG-9328 and other unreported bugs are fixed, it needs to be a thing.
*/
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
#include <errno.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
template <typename T>
bool IndirectOpen(T callable, qint64* pid_forked = nullptr)
{
auto pid = fork();
if (pid_forked) {
if (pid > 0)
*pid_forked = pid;
else
*pid_forked = 0;
}
if (pid == -1) {
qWarning() << "IndirectOpen failed to fork: " << errno;
return false;
}
// child - do the stuff
if (pid == 0) {
// unset all this garbage so it doesn't get passed to the child process
qunsetenv("LD_PRELOAD");
qunsetenv("LD_LIBRARY_PATH");
qunsetenv("LD_DEBUG");
qunsetenv("QT_PLUGIN_PATH");
qunsetenv("QT_FONTPATH");
// open the URL
auto status = callable();
// detach from the parent process group.
setsid();
// die. now. do not clean up anything, it would just hang forever.
_exit(status ? 0 : 1);
} else {
// parent - assume it worked.
int status;
while (waitpid(pid, &status, 0)) {
if (WIFEXITED(status)) {
return WEXITSTATUS(status) == 0;
}
if (WIFSIGNALED(status)) {
return false;
}
}
return true;
}
}
#endif
namespace DesktopServices { namespace DesktopServices {
bool openDirectory(const QString& path, [[maybe_unused]] bool ensureExists) bool openPath(const QFileInfo& path, bool ensureFolderPathExists)
{ {
qDebug() << "Opening directory" << path; qDebug() << "Opening path" << path;
QDir parentPath; if (ensureFolderPathExists) {
QDir dir(path); FS::ensureFolderPathExists(path);
if (ensureExists && !dir.exists()) {
parentPath.mkpath(dir.absolutePath());
} }
auto f = [&]() { return QDesktopServices::openUrl(QUrl::fromLocalFile(dir.absolutePath())); }; return openUrl(QUrl::fromLocalFile(QFileInfo(path).absoluteFilePath()));
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
if (!isSandbox()) {
return IndirectOpen(f);
}
#endif
return f();
} }
bool openFile(const QString& path) bool openPath(const QString& path, bool ensureFolderPathExists)
{ {
qDebug() << "Opening file" << path; return openPath(QFileInfo(path), ensureFolderPathExists);
auto f = [&]() { return QDesktopServices::openUrl(QUrl::fromLocalFile(path)); };
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
if (!isSandbox()) {
return IndirectOpen(f);
} else {
return f();
}
#else
return f();
#endif
}
bool openFile(const QString& application, const QString& path, const QString& workingDirectory, qint64* pid)
{
qDebug() << "Opening file" << path << "using" << application;
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
// FIXME: the pid here is fake. So if something depends on it, it will likely misbehave
if (!isSandbox()) {
return IndirectOpen([&]() { return QProcess::startDetached(application, QStringList() << path, workingDirectory); }, pid);
} else {
return QProcess::startDetached(application, QStringList() << path, workingDirectory, pid);
}
#else
return QProcess::startDetached(application, QStringList() << path, workingDirectory, pid);
#endif
} }
bool run(const QString& application, const QStringList& args, const QString& workingDirectory, qint64* pid) bool run(const QString& application, const QStringList& args, const QString& workingDirectory, qint64* pid)
{ {
qDebug() << "Running" << application << "with args" << args.join(' '); qDebug() << "Running" << application << "with args" << args.join(' ');
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
if (!isSandbox()) {
// FIXME: the pid here is fake. So if something depends on it, it will likely misbehave
return IndirectOpen([&]() { return QProcess::startDetached(application, args, workingDirectory); }, pid);
} else {
return QProcess::startDetached(application, args, workingDirectory, pid);
}
#else
return QProcess::startDetached(application, args, workingDirectory, pid); return QProcess::startDetached(application, args, workingDirectory, pid);
#endif
} }
bool openUrl(const QUrl& url) bool openUrl(const QUrl& url)
{ {
qDebug() << "Opening URL" << url.toString(); qDebug() << "Opening URL" << url.toString();
auto f = [&]() { return QDesktopServices::openUrl(url); }; return QDesktopServices::openUrl(url);
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
if (!isSandbox()) {
return IndirectOpen(f);
} else {
return f();
}
#else
return f();
#endif
} }
bool isFlatpak() bool isFlatpak()
@ -191,9 +84,4 @@ bool isSnap()
#endif #endif
} }
bool isSandbox()
{
return isSnap() || isFlatpak();
}
} // namespace DesktopServices } // namespace DesktopServices

View File

@ -3,31 +3,30 @@
#include <QString> #include <QString>
#include <QUrl> #include <QUrl>
class QFileInfo;
/** /**
* This wraps around QDesktopServices and adds workarounds where needed * This wraps around QDesktopServices and adds workarounds where needed
* Use this instead of QDesktopServices! * Use this instead of QDesktopServices!
*/ */
namespace DesktopServices { namespace DesktopServices {
/** /**
* Open a file in whatever application is applicable * Open a path in whatever application is applicable.
* @param ensureFolderPathExists Make sure the path exists
*/ */
bool openFile(const QString& path); bool openPath(const QFileInfo& path, bool ensureFolderPathExists = false);
/** /**
* Open a file in the specified application * Open a path in whatever application is applicable.
* @param ensureFolderPathExists Make sure the path exists
*/ */
bool openFile(const QString& application, const QString& path, const QString& workingDirectory = QString(), qint64* pid = 0); bool openPath(const QString& path, bool ensureFolderPathExists = false);
/** /**
* Run an application * Run an application
*/ */
bool run(const QString& application, const QStringList& args, const QString& workingDirectory = QString(), qint64* pid = 0); bool run(const QString& application, const QStringList& args, const QString& workingDirectory = QString(), qint64* pid = 0);
/**
* Open a directory
*/
bool openDirectory(const QString& path, bool ensureExists = false);
/** /**
* Open the URL, most likely in a browser. Maybe. * Open the URL, most likely in a browser. Maybe.
*/ */
@ -42,9 +41,4 @@ bool isFlatpak();
* Determine whether the launcher is running in a Snap environment * Determine whether the launcher is running in a Snap environment
*/ */
bool isSnap(); bool isSnap();
/**
* Determine whether the launcher is running in a sandboxed (Flatpak or Snap) environment
*/
bool isSandbox();
} // namespace DesktopServices } // namespace DesktopServices

View File

@ -1,4 +1,37 @@
// Licensed under the Apache-2.0 license. See README.md for details. // SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2024 TheKodeToad <TheKodeToad@proton.me>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once #pragma once
@ -8,12 +41,12 @@
class Exception : public std::exception { class Exception : public std::exception {
public: public:
Exception(const QString& message) : std::exception(), m_message(message) { qCritical() << "Exception:" << message; } Exception(const QString& message) : std::exception(), m_message(message.toUtf8()) { qCritical() << "Exception:" << message; }
Exception(const Exception& other) : std::exception(), m_message(other.cause()) {} Exception(const Exception& other) : std::exception(), m_message(other.m_message) {}
virtual ~Exception() noexcept {} virtual ~Exception() noexcept {}
const char* what() const noexcept { return m_message.toLatin1().constData(); } const char* what() const noexcept { return m_message.constData(); }
QString cause() const { return m_message; } QString cause() const { return QString::fromUtf8(m_message); }
private: private:
QString m_message; QByteArray m_message;
}; };

View File

@ -272,15 +272,19 @@ bool ensureFilePathExists(QString filenamepath)
return success; return success;
} }
bool ensureFolderPathExists(QString foldernamepath) bool ensureFolderPathExists(const QFileInfo folderPath)
{ {
QFileInfo a(foldernamepath);
QDir dir; QDir dir;
QString ensuredPath = a.filePath(); QString ensuredPath = folderPath.filePath();
bool success = dir.mkpath(ensuredPath); bool success = dir.mkpath(ensuredPath);
return success; return success;
} }
bool ensureFolderPathExists(const QString folderPathName)
{
return ensureFolderPathExists(QFileInfo(folderPathName));
}
bool copyFileAttributes(QString src, QString dst) bool copyFileAttributes(QString src, QString dst)
{ {
#ifdef Q_OS_WIN32 #ifdef Q_OS_WIN32
@ -797,15 +801,24 @@ QString NormalizePath(QString path)
} }
} }
QString badFilenameChars = "\"\\/?<>:;*|!+\r\n"; static const QString BAD_PATH_CHARS = "\"?<>:;*|!+\r\n";
static const QString BAD_FILENAME_CHARS = BAD_PATH_CHARS + "\\/";
QString RemoveInvalidFilenameChars(QString string, QChar replaceWith) QString RemoveInvalidFilenameChars(QString string, QChar replaceWith)
{ {
for (int i = 0; i < string.length(); i++) { for (int i = 0; i < string.length(); i++)
if (badFilenameChars.contains(string[i])) { if (string.at(i) < ' ' || BAD_FILENAME_CHARS.contains(string.at(i)))
string[i] = replaceWith; string[i] = replaceWith;
}
} return string;
}
QString RemoveInvalidPathChars(QString string, QChar replaceWith)
{
for (int i = 0; i < string.length(); i++)
if (string.at(i) < ' ' || BAD_PATH_CHARS.contains(string.at(i)))
string[i] = replaceWith;
return string; return string;
} }
@ -1581,4 +1594,44 @@ uintmax_t hardLinkCount(const QString& path)
return count; return count;
} }
#ifdef Q_OS_WIN
// returns 8.3 file format from long path
QString shortPathName(const QString& file)
{
auto input = file.toStdWString();
std::wstring output;
long length = GetShortPathNameW(input.c_str(), NULL, 0);
if (length == 0)
return {};
// NOTE: this resizing might seem weird...
// when GetShortPathNameW fails, it returns length including null character
// when it succeeds, it returns length excluding null character
// See: https://msdn.microsoft.com/en-us/library/windows/desktop/aa364989(v=vs.85).aspx
output.resize(length);
if (GetShortPathNameW(input.c_str(), (LPWSTR)output.c_str(), length) == 0)
return {};
output.resize(length - 1);
QString ret = QString::fromStdWString(output);
return ret;
}
// if the string survives roundtrip through local 8bit encoding...
bool fitsInLocal8bit(const QString& string)
{
return string == QString::fromLocal8Bit(string.toLocal8Bit());
}
QString getPathNameInLocal8bit(const QString& file)
{
if (!fitsInLocal8bit(file)) {
auto path = shortPathName(file);
if (!path.isEmpty()) {
return path;
}
// in case shortPathName fails just return the path as is
}
return file;
}
#endif
} // namespace FS } // namespace FS

View File

@ -91,7 +91,13 @@ bool ensureFilePathExists(QString filenamepath);
* Creates all the folders in a path for the specified path * Creates all the folders in a path for the specified path
* last segment of the path is treated as a folder name and is created! * last segment of the path is treated as a folder name and is created!
*/ */
bool ensureFolderPathExists(QString filenamepath); bool ensureFolderPathExists(const QFileInfo folderPath);
/**
* Creates all the folders in a path for the specified path
* last segment of the path is treated as a folder name and is created!
*/
bool ensureFolderPathExists(const QString folderPathName);
/** /**
* @brief Copies a directory and it's contents from src to dest * @brief Copies a directory and it's contents from src to dest
@ -336,6 +342,8 @@ QString NormalizePath(QString path);
QString RemoveInvalidFilenameChars(QString string, QChar replaceWith = '-'); QString RemoveInvalidFilenameChars(QString string, QChar replaceWith = '-');
QString RemoveInvalidPathChars(QString string, QChar replaceWith = '-');
QString DirNameFromString(QString string, QString inDir = "."); QString DirNameFromString(QString string, QString inDir = ".");
/// Checks if the a given Path contains "!" /// Checks if the a given Path contains "!"
@ -545,4 +553,8 @@ bool canLink(const QString& src, const QString& dst);
uintmax_t hardLinkCount(const QString& path); uintmax_t hardLinkCount(const QString& path);
#ifdef Q_OS_WIN
QString getPathNameInLocal8bit(const QString& file);
#endif
} // namespace FS } // namespace FS

View File

@ -43,10 +43,10 @@ void InstanceCopyTask::executeTask()
QFileInfo dotMCDir(FS::PathCombine(m_stagingPath, ".minecraft")); QFileInfo dotMCDir(FS::PathCombine(m_stagingPath, ".minecraft"));
QString staging_mc_dir; QString staging_mc_dir;
if (mcDir.exists() && !dotMCDir.exists()) if (dotMCDir.exists() && !mcDir.exists())
staging_mc_dir = mcDir.filePath();
else
staging_mc_dir = dotMCDir.filePath(); staging_mc_dir = dotMCDir.filePath();
else
staging_mc_dir = mcDir.filePath();
FS::copy savesCopy(FS::PathCombine(m_origInstance->gameRoot(), "saves"), FS::PathCombine(staging_mc_dir, "saves")); FS::copy savesCopy(FS::PathCombine(m_origInstance->gameRoot(), "saves"), FS::PathCombine(staging_mc_dir, "saves"));
savesCopy.followSymlinks(true); savesCopy.followSymlinks(true);

View File

@ -164,8 +164,8 @@ void InstanceImportTask::processZipPack()
} else if (technicFound) { } else if (technicFound) {
// process as Technic pack // process as Technic pack
qDebug() << "Technic:" << technicFound; qDebug() << "Technic:" << technicFound;
extractDir.mkpath(".minecraft"); extractDir.mkpath("minecraft");
extractDir.cd(".minecraft"); extractDir.cd("minecraft");
m_modpackType = ModpackType::Technic; m_modpackType = ModpackType::Technic;
} else { } else {
QStringList paths_to_ignore{ "overrides/" }; QStringList paths_to_ignore{ "overrides/" };

View File

@ -47,9 +47,6 @@
#include <optional> #include <optional>
class QuaZip; class QuaZip;
namespace Flame {
class FileResolvingTask;
}
class InstanceImportTask : public InstanceTask { class InstanceImportTask : public InstanceTask {
Q_OBJECT Q_OBJECT
@ -79,7 +76,6 @@ class InstanceImportTask : public InstanceTask {
private: /* data */ private: /* data */
NetJob::Ptr m_filesNetJob; NetJob::Ptr m_filesNetJob;
shared_qobject_ptr<Flame::FileResolvingTask> m_modIdResolver;
QUrl m_sourceUrl; QUrl m_sourceUrl;
QString m_archivePath; QString m_archivePath;
bool m_downloadRequired = false; bool m_downloadRequired = false;

View File

@ -38,6 +38,7 @@
#include <QDir> #include <QDir>
#include <QDirIterator> #include <QDirIterator>
#include <QFile> #include <QFile>
#include <QFileInfo>
#include <QFileSystemWatcher> #include <QFileSystemWatcher>
#include <QJsonArray> #include <QJsonArray>
#include <QJsonDocument> #include <QJsonDocument>
@ -847,14 +848,16 @@ class InstanceStaging : public Task {
const unsigned maxBackoff = 16; const unsigned maxBackoff = 16;
public: public:
InstanceStaging(InstanceList* parent, InstanceTask* child, QString stagingPath, InstanceName const& instanceName, QString groupName) InstanceStaging(InstanceList* parent, InstanceTask* child, SettingsObjectPtr settings)
: m_parent(parent) : m_parent(parent), backoff(minBackoff, maxBackoff)
, backoff(minBackoff, maxBackoff)
, m_stagingPath(std::move(stagingPath))
, m_instance_name(std::move(instanceName))
, m_groupName(std::move(groupName))
{ {
m_stagingPath = parent->getStagedInstancePath();
m_child.reset(child); m_child.reset(child);
m_child->setStagingPath(m_stagingPath);
m_child->setParentSettings(std::move(settings));
connect(child, &Task::succeeded, this, &InstanceStaging::childSucceeded); connect(child, &Task::succeeded, this, &InstanceStaging::childSucceeded);
connect(child, &Task::failed, this, &InstanceStaging::childFailed); connect(child, &Task::failed, this, &InstanceStaging::childFailed);
connect(child, &Task::aborted, this, &InstanceStaging::childAborted); connect(child, &Task::aborted, this, &InstanceStaging::childAborted);
@ -866,7 +869,7 @@ class InstanceStaging : public Task {
connect(&m_backoffTimer, &QTimer::timeout, this, &InstanceStaging::childSucceeded); connect(&m_backoffTimer, &QTimer::timeout, this, &InstanceStaging::childSucceeded);
} }
virtual ~InstanceStaging(){}; virtual ~InstanceStaging() {}
// FIXME/TODO: add ability to abort during instance commit retries // FIXME/TODO: add ability to abort during instance commit retries
bool abort() override bool abort() override
@ -881,14 +884,22 @@ class InstanceStaging : public Task {
bool canAbort() const override { return (m_child && m_child->canAbort()); } bool canAbort() const override { return (m_child && m_child->canAbort()); }
protected: protected:
virtual void executeTask() override { m_child->start(); } virtual void executeTask() override
{
if (m_stagingPath.isNull()) {
emitFailed(tr("Could not create staging folder"));
return;
}
m_child->start();
}
QStringList warnings() const override { return m_child->warnings(); } QStringList warnings() const override { return m_child->warnings(); }
private slots: private slots:
void childSucceeded() void childSucceeded()
{ {
unsigned sleepTime = backoff(); unsigned sleepTime = backoff();
if (m_parent->commitStagedInstance(m_stagingPath, m_instance_name, m_groupName, *m_child.get())) { if (m_parent->commitStagedInstance(m_stagingPath, *m_child.get(), m_child->group(), *m_child.get())) {
emitSucceeded(); emitSucceeded();
return; return;
} }
@ -897,7 +908,7 @@ class InstanceStaging : public Task {
emitFailed(tr("Failed to commit instance, even after multiple retries. It is being blocked by something.")); emitFailed(tr("Failed to commit instance, even after multiple retries. It is being blocked by something."));
return; return;
} }
qDebug() << "Failed to commit instance" << m_instance_name.name() << "Initiating backoff:" << sleepTime; qDebug() << "Failed to commit instance" << m_child->name() << "Initiating backoff:" << sleepTime;
m_backoffTimer.start(sleepTime * 500); m_backoffTimer.start(sleepTime * 500);
} }
void childFailed(const QString& reason) void childFailed(const QString& reason)
@ -906,7 +917,11 @@ class InstanceStaging : public Task {
emitFailed(reason); emitFailed(reason);
} }
void childAborted() { emitAborted(); } void childAborted()
{
m_parent->destroyStagingPath(m_stagingPath);
emitAborted();
}
private: private:
InstanceList* m_parent; InstanceList* m_parent;
@ -918,34 +933,35 @@ class InstanceStaging : public Task {
ExponentialSeries backoff; ExponentialSeries backoff;
QString m_stagingPath; QString m_stagingPath;
unique_qobject_ptr<InstanceTask> m_child; unique_qobject_ptr<InstanceTask> m_child;
InstanceName m_instance_name;
QString m_groupName;
QTimer m_backoffTimer; QTimer m_backoffTimer;
}; };
Task* InstanceList::wrapInstanceTask(InstanceTask* task) Task* InstanceList::wrapInstanceTask(InstanceTask* task)
{ {
auto stagingPath = getStagedInstancePath(); return new InstanceStaging(this, task, m_globalSettings);
task->setStagingPath(stagingPath);
task->setParentSettings(m_globalSettings);
return new InstanceStaging(this, task, stagingPath, *task, task->group());
} }
QString InstanceList::getStagedInstancePath() QString InstanceList::getStagedInstancePath()
{ {
QString key = QUuid::createUuid().toString(QUuid::WithoutBraces); const QString tempRoot = FS::PathCombine(m_instDir, ".tmp");
QString tempDir = ".LAUNCHER_TEMP/";
QString relPath = FS::PathCombine(tempDir, key); QString result;
QDir rootPath(m_instDir); int tries = 0;
auto path = FS::PathCombine(m_instDir, relPath);
if (!rootPath.mkpath(relPath)) { do {
return QString(); if (++tries > 256)
} return {};
const QString key = QUuid::createUuid().toString(QUuid::Id128).left(6);
result = FS::PathCombine(tempRoot, key);
} while (QFileInfo::exists(result));
if (!QDir::current().mkpath(result))
return {};
#ifdef Q_OS_WIN32 #ifdef Q_OS_WIN32
auto tempPath = FS::PathCombine(m_instDir, tempDir); SetFileAttributesA(tempRoot.toStdString().c_str(), FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED);
SetFileAttributesA(tempPath.toStdString().c_str(), FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED);
#endif #endif
return path; return result;
} }
bool InstanceList::commitStagedInstance(const QString& path, bool InstanceList::commitStagedInstance(const QString& path,

View File

@ -2,6 +2,8 @@
#include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/CustomMessageBox.h"
#include <QPushButton>
InstanceNameChange askForChangingInstanceName(QWidget* parent, const QString& old_name, const QString& new_name) InstanceNameChange askForChangingInstanceName(QWidget* parent, const QString& old_name, const QString& new_name)
{ {
auto dialog = auto dialog =
@ -27,16 +29,15 @@ ShouldUpdate askIfShouldUpdate(QWidget* parent, QString original_version_name)
"separate instance, or update the existing one?\n\nNOTE: Make sure you made a backup of your important instance data before " "separate instance, or update the existing one?\n\nNOTE: Make sure you made a backup of your important instance data before "
"updating, as worlds can be corrupted and some configuration may be lost (due to pack overrides).") "updating, as worlds can be corrupted and some configuration may be lost (due to pack overrides).")
.arg(original_version_name), .arg(original_version_name),
QMessageBox::Information, QMessageBox::Ok | QMessageBox::Reset | QMessageBox::Abort); QMessageBox::Information, QMessageBox::Cancel);
info->setButtonText(QMessageBox::Ok, QObject::tr("Update existing instance")); QAbstractButton* update = info->addButton(QObject::tr("Update existing instance"), QMessageBox::AcceptRole);
info->setButtonText(QMessageBox::Abort, QObject::tr("Create new instance")); QAbstractButton* skip = info->addButton(QObject::tr("Create new instance"), QMessageBox::ResetRole);
info->setButtonText(QMessageBox::Reset, QObject::tr("Cancel"));
info->exec(); info->exec();
if (info->clickedButton() == info->button(QMessageBox::Ok)) if (info->clickedButton() == update)
return ShouldUpdate::Update; return ShouldUpdate::Update;
if (info->clickedButton() == info->button(QMessageBox::Abort)) if (info->clickedButton() == skip)
return ShouldUpdate::SkipUpdating; return ShouldUpdate::SkipUpdating;
return ShouldUpdate::Cancel; return ShouldUpdate::Cancel;
} }

View File

@ -42,7 +42,6 @@
#include "ui/InstanceWindow.h" #include "ui/InstanceWindow.h"
#include "ui/MainWindow.h" #include "ui/MainWindow.h"
#include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/CustomMessageBox.h"
#include "ui/dialogs/EditAccountDialog.h"
#include "ui/dialogs/ProfileSelectDialog.h" #include "ui/dialogs/ProfileSelectDialog.h"
#include "ui/dialogs/ProfileSetupDialog.h" #include "ui/dialogs/ProfileSetupDialog.h"
#include "ui/dialogs/ProgressDialog.h" #include "ui/dialogs/ProgressDialog.h"
@ -144,6 +143,12 @@ void LaunchController::login()
bool tryagain = true; bool tryagain = true;
unsigned int tries = 0; unsigned int tries = 0;
if (m_accountToUse->accountType() != AccountType::Offline && m_accountToUse->accountState() == AccountState::Offline) {
// Force account refresh on the account used to launch the instance updating the AccountState
// only on first try and if it is not meant to be offline
auto accounts = APPLICATION->accounts();
accounts->requestRefresh(m_accountToUse->internalId());
}
while (tryagain) { while (tryagain) {
if (tries > 0 && tries % 3 == 0) { if (tries > 0 && tries % 3 == 0) {
auto result = auto result =
@ -250,12 +255,6 @@ void LaunchController::login()
progDialog.execWithTask(task.get()); progDialog.execWithTask(task.get());
continue; continue;
} }
// FIXME: this is missing - the meaning is that the account is queued for refresh and we should wait for that
/*
case AccountState::Queued: {
return;
}
*/
case AccountState::Expired: { case AccountState::Expired: {
auto errorString = tr("The account has expired and needs to be logged into manually again."); auto errorString = tr("The account has expired and needs to be logged into manually again.");
QMessageBox::warning(m_parentWidget, tr("Account refresh failed"), errorString, QMessageBox::StandardButton::Ok, QMessageBox::warning(m_parentWidget, tr("Account refresh failed"), errorString, QMessageBox::StandardButton::Ok,

View File

@ -119,6 +119,7 @@ bool compressDirFiles(QuaZip* zip, QString dir, QFileInfoList files, bool follow
bool 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);
zip.setUtf8Enabled(true);
QDir().mkpath(QFileInfo(fileCompressed).absolutePath()); QDir().mkpath(QFileInfo(fileCompressed).absolutePath());
if (!zip.open(QuaZip::mdCreate)) { if (!zip.open(QuaZip::mdCreate)) {
QFile::remove(fileCompressed); QFile::remove(fileCompressed);
@ -141,6 +142,7 @@ bool compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files,
bool 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);
zipOut.setUtf8Enabled(true);
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";
@ -286,10 +288,13 @@ std::optional<QStringList> extractSubDir(QuaZip* zip, const QString& subdir, con
do { do {
QString file_name = zip->getCurrentFileName(); QString file_name = zip->getCurrentFileName();
#ifdef Q_OS_WIN
file_name = FS::RemoveInvalidPathChars(file_name);
#endif
if (!file_name.startsWith(subdir)) if (!file_name.startsWith(subdir))
continue; continue;
auto relative_file_name = QDir::fromNativeSeparators(file_name.remove(0, subdir.size())); auto relative_file_name = QDir::fromNativeSeparators(file_name.mid(subdir.size()));
auto original_name = relative_file_name; auto original_name = relative_file_name;
// Fix subdirs/files ending with a / getting transformed into absolute paths // Fix subdirs/files ending with a / getting transformed into absolute paths
@ -463,7 +468,7 @@ auto ExportToZipTask::exportZip() -> ZipResult
auto absolute = file.absoluteFilePath(); auto absolute = file.absoluteFilePath();
auto relative = m_dir.relativeFilePath(absolute); auto relative = m_dir.relativeFilePath(absolute);
setStatus("Compresing: " + relative); setStatus("Compressing: " + relative);
setProgress(m_progress + 1, m_progressTotal); setProgress(m_progress + 1, m_progressTotal);
if (m_follow_symlinks) { if (m_follow_symlinks) {
if (file.isSymLink()) if (file.isSymLink())

View File

@ -163,6 +163,7 @@ class ExportToZipTask : public Task {
, m_follow_symlinks(followSymlinks) , m_follow_symlinks(followSymlinks)
{ {
setAbortable(true); setAbortable(true);
m_output.setUtf8Enabled(true);
}; };
ExportToZipTask(QString outputPath, QString dir, QFileInfoList files, QString destinationPrefix = "", bool followSymlinks = false) ExportToZipTask(QString outputPath, QString dir, QFileInfoList files, QString destinationPrefix = "", bool followSymlinks = false)
: ExportToZipTask(outputPath, QDir(dir), files, destinationPrefix, followSymlinks){}; : ExportToZipTask(outputPath, QDir(dir), files, destinationPrefix, followSymlinks){};

View File

@ -55,6 +55,9 @@ void JavaChecker::performCheck()
qDebug() << "Java checker library could not be found. Please check your installation."; qDebug() << "Java checker library could not be found. Please check your installation.";
return; return;
} }
#ifdef Q_OS_WIN
checkerJar = FS::getPathNameInLocal8bit(checkerJar);
#endif
QStringList args; QStringList args;

View File

@ -173,11 +173,12 @@ void MinecraftInstance::loadSpecificSettings()
m_settings->registerOverride(global_settings->getSetting("UseNativeGLFW"), nativeLibraryWorkaroundsOverride); m_settings->registerOverride(global_settings->getSetting("UseNativeGLFW"), nativeLibraryWorkaroundsOverride);
m_settings->registerOverride(global_settings->getSetting("CustomGLFWPath"), nativeLibraryWorkaroundsOverride); m_settings->registerOverride(global_settings->getSetting("CustomGLFWPath"), nativeLibraryWorkaroundsOverride);
// Peformance related options // Performance related options
auto performanceOverride = m_settings->registerSetting("OverridePerformance", false); auto performanceOverride = m_settings->registerSetting("OverridePerformance", false);
m_settings->registerOverride(global_settings->getSetting("EnableFeralGamemode"), performanceOverride); m_settings->registerOverride(global_settings->getSetting("EnableFeralGamemode"), performanceOverride);
m_settings->registerOverride(global_settings->getSetting("EnableMangoHud"), performanceOverride); m_settings->registerOverride(global_settings->getSetting("EnableMangoHud"), performanceOverride);
m_settings->registerOverride(global_settings->getSetting("UseDiscreteGpu"), performanceOverride); m_settings->registerOverride(global_settings->getSetting("UseDiscreteGpu"), performanceOverride);
m_settings->registerOverride(global_settings->getSetting("UseZink"), performanceOverride);
// Miscellaneous // Miscellaneous
auto miscellaneousOverride = m_settings->registerSetting("OverrideMiscellaneous", false); auto miscellaneousOverride = m_settings->registerSetting("OverrideMiscellaneous", false);
@ -292,10 +293,10 @@ QString MinecraftInstance::gameRoot() const
QFileInfo mcDir(FS::PathCombine(instanceRoot(), "minecraft")); QFileInfo mcDir(FS::PathCombine(instanceRoot(), "minecraft"));
QFileInfo dotMCDir(FS::PathCombine(instanceRoot(), ".minecraft")); QFileInfo dotMCDir(FS::PathCombine(instanceRoot(), ".minecraft"));
if (mcDir.exists() && !dotMCDir.exists()) if (dotMCDir.exists() && !mcDir.exists())
return mcDir.filePath();
else
return dotMCDir.filePath(); return dotMCDir.filePath();
else
return mcDir.filePath();
} }
QString MinecraftInstance::binRoot() const QString MinecraftInstance::binRoot() const
@ -594,9 +595,6 @@ QProcessEnvironment MinecraftInstance::createLaunchEnvironment()
QStringList preloadList; QStringList preloadList;
if (auto value = env.value("LD_PRELOAD"); !value.isEmpty()) if (auto value = env.value("LD_PRELOAD"); !value.isEmpty())
preloadList = value.split(QLatin1String(":")); preloadList = value.split(QLatin1String(":"));
QStringList libPaths;
if (auto value = env.value("LD_LIBRARY_PATH"); !value.isEmpty())
libPaths = value.split(QLatin1String(":"));
auto mangoHudLibString = MangoHud::getLibraryString(); auto mangoHudLibString = MangoHud::getLibraryString();
if (!mangoHudLibString.isEmpty()) { if (!mangoHudLibString.isEmpty()) {
@ -604,18 +602,16 @@ QProcessEnvironment MinecraftInstance::createLaunchEnvironment()
QString libPath = mangoHudLib.absolutePath(); QString libPath = mangoHudLib.absolutePath();
auto appendLib = [libPath, &preloadList](QString fileName) { auto appendLib = [libPath, &preloadList](QString fileName) {
if (QFileInfo(FS::PathCombine(libPath, fileName)).exists()) if (QFileInfo(FS::PathCombine(libPath, fileName)).exists())
preloadList << fileName; preloadList << FS::PathCombine(libPath, fileName);
}; };
// dlsym variant is only needed for OpenGL and not included in the vulkan layer // dlsym variant is only needed for OpenGL and not included in the vulkan layer
appendLib("libMangoHud_dlsym.so"); appendLib("libMangoHud_dlsym.so");
appendLib("libMangoHud_opengl.so"); appendLib("libMangoHud_opengl.so");
appendLib(mangoHudLib.fileName()); appendLib(mangoHudLib.fileName());
libPaths << libPath;
} }
env.insert("LD_PRELOAD", preloadList.join(QLatin1String(":"))); env.insert("LD_PRELOAD", preloadList.join(QLatin1String(":")));
env.insert("LD_LIBRARY_PATH", libPaths.join(QLatin1String(":")));
env.insert("MANGOHUD", "1"); env.insert("MANGOHUD", "1");
} }
@ -627,6 +623,13 @@ QProcessEnvironment MinecraftInstance::createLaunchEnvironment()
env.insert("__VK_LAYER_NV_optimus", "NVIDIA_only"); env.insert("__VK_LAYER_NV_optimus", "NVIDIA_only");
env.insert("__GLX_VENDOR_LIBRARY_NAME", "nvidia"); env.insert("__GLX_VENDOR_LIBRARY_NAME", "nvidia");
} }
if (settings()->get("UseZink").toBool()) {
// taken from https://wiki.archlinux.org/title/OpenGL#OpenGL_over_Vulkan_(Zink)
env.insert("__GLX_VENDOR_LIBRARY_NAME", "mesa");
env.insert("MESA_LOADER_DRIVER_OVERRIDE", "zink");
env.insert("GALLIUM_DRIVER", "zink");
}
#endif #endif
return env; return env;
} }
@ -662,8 +665,12 @@ QStringList MinecraftInstance::processMinecraftArgs(AuthSessionPtr session, Mine
} }
if (serverToJoin && !serverToJoin->address.isEmpty()) { if (serverToJoin && !serverToJoin->address.isEmpty()) {
args_pattern += " --server " + serverToJoin->address; if (profile->hasTrait("feature:is_quick_play_multiplayer")) {
args_pattern += " --port " + QString::number(serverToJoin->port); args_pattern += " --quickPlayMultiplayer " + serverToJoin->address + ':' + QString::number(serverToJoin->port);
} else {
args_pattern += " --server " + serverToJoin->address;
args_pattern += " --port " + QString::number(serverToJoin->port);
}
} }
QMap<QString, QString> token_mapping; QMap<QString, QString> token_mapping;

View File

@ -157,20 +157,6 @@ void MojangVersionFormat::readVersionProperties(const QJsonObject& in, VersionFi
Bits::readString(in, "id", out->minecraftVersion); Bits::readString(in, "id", out->minecraftVersion);
Bits::readString(in, "mainClass", out->mainClass); Bits::readString(in, "mainClass", out->mainClass);
Bits::readString(in, "minecraftArguments", out->minecraftArguments); Bits::readString(in, "minecraftArguments", out->minecraftArguments);
if (out->minecraftArguments.isEmpty()) {
QString processArguments;
Bits::readString(in, "processArguments", processArguments);
QString toCompare = processArguments.toLower();
if (toCompare == "legacy") {
out->minecraftArguments = " ${auth_player_name} ${auth_session}";
} else if (toCompare == "username_session") {
out->minecraftArguments = "--username ${auth_player_name} --session ${auth_session}";
} else if (toCompare == "username_session_version") {
out->minecraftArguments = "--username ${auth_player_name} --session ${auth_session} --version ${profile_name}";
} else if (!toCompare.isEmpty()) {
out->addProblem(ProblemSeverity::Error, QObject::tr("processArguments is set to unknown value '%1'").arg(processArguments));
}
}
Bits::readString(in, "type", out->type); Bits::readString(in, "type", out->type);
Bits::readString(in, "assets", out->assets); Bits::readString(in, "assets", out->assets);

View File

@ -52,8 +52,6 @@
#include <FileSystem.h> #include <FileSystem.h>
#include <QSaveFile> #include <QSaveFile>
#include <chrono>
enum AccountListVersion { MojangMSA = 3 }; enum AccountListVersion { MojangMSA = 3 };
AccountList::AccountList(QObject* parent) : QAbstractListModel(parent) AccountList::AccountList(QObject* parent) : QAbstractListModel(parent)

View File

@ -126,7 +126,35 @@ bool XboxAuthorizationStep::processSTSError(QNetworkReply::NetworkError error, Q
emit finished( emit finished(
AccountTaskState::STATE_FAILED_SOFT, AccountTaskState::STATE_FAILED_SOFT,
tr("This Microsoft account is underaged and is not linked to a family.\n\nPlease set up your account according to %1.") tr("This Microsoft account is underaged and is not linked to a family.\n\nPlease set up your account according to %1.")
.arg("<a href=\"https://help.minecraft.net/hc/en-us/articles/4403181904525\">help.minecraft.net</a>")); .arg("<a href=\"https://help.minecraft.net/hc/en-us/articles/4408968616077\">help.minecraft.net</a>"));
return true;
}
// the following codes where copied from: https://github.com/PrismarineJS/prismarine-auth/pull/44
case 2148916236: {
emit finished(AccountTaskState::STATE_FAILED_SOFT,
tr("This Microsoft account requires proof of age to play. Please login to %1 to provide proof of age.")
.arg("<a href=\"https://login.live.com/login.srf\">login.live.com</a>"));
return true;
}
case 2148916237:
emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("This Microsoft account has reached its limit for playtime. This "
"Microsoft account has been blocked from logging in."));
return true;
case 2148916227: {
emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("This Microsoft account was banned by Xbox for violating one or more "
"Community Standards for Xbox and is unable to be used."));
return true;
}
case 2148916229: {
emit finished(AccountTaskState::STATE_FAILED_SOFT,
tr("This Microsoft account is currently restricted and your guardian has not given you permission to play "
"online. Login to %1 and have your guardian change your permissions.")
.arg("<a href=\"https://account.microsoft.com/family/\">account.microsoft.com</a>"));
return true;
}
case 2148916234: {
emit finished(AccountTaskState::STATE_FAILED_SOFT,
tr("This Microsoft account has not accepted Xbox's Terms of Service. Please login and accept them."));
return true; return true;
} }
default: { default: {

View File

@ -79,6 +79,7 @@ void ExtractNatives::executeTask()
auto settings = minecraftInstance->settings(); auto settings = minecraftInstance->settings();
auto outputPath = minecraftInstance->getNativePath(); auto outputPath = minecraftInstance->getNativePath();
FS::ensureFolderPathExists(outputPath);
auto javaVersion = minecraftInstance->getJavaVersion(); auto javaVersion = minecraftInstance->getJavaVersion();
bool jniHackEnabled = javaVersion.major() >= 8; bool jniHackEnabled = javaVersion.major() >= 8;
for (const auto& source : toExtract) { for (const auto& source : toExtract) {

View File

@ -16,8 +16,6 @@
#pragma once #pragma once
#include <launch/LaunchStep.h> #include <launch/LaunchStep.h>
#include <memory>
#include "minecraft/auth/AuthSession.h"
// FIXME: temporary wrapper for existing task. // FIXME: temporary wrapper for existing task.
class ExtractNatives : public LaunchStep { class ExtractNatives : public LaunchStep {

View File

@ -66,32 +66,6 @@ LauncherPartLaunch::LauncherPartLaunch(LaunchTask* parent) : LaunchStep(parent)
connect(&m_process, &LoggedProcess::stateChanged, this, &LauncherPartLaunch::on_state); connect(&m_process, &LoggedProcess::stateChanged, this, &LauncherPartLaunch::on_state);
} }
#ifdef Q_OS_WIN
// returns 8.3 file format from long path
#include <windows.h>
QString shortPathName(const QString& file)
{
auto input = file.toStdWString();
std::wstring output;
long length = GetShortPathNameW(input.c_str(), NULL, 0);
// NOTE: this resizing might seem weird...
// when GetShortPathNameW fails, it returns length including null character
// when it succeeds, it returns length excluding null character
// See: https://msdn.microsoft.com/en-us/library/windows/desktop/aa364989(v=vs.85).aspx
output.resize(length);
GetShortPathNameW(input.c_str(), (LPWSTR)output.c_str(), length);
output.resize(length - 1);
QString ret = QString::fromStdWString(output);
return ret;
}
#endif
// if the string survives roundtrip through local 8bit encoding...
bool fitsInLocal8bit(const QString& string)
{
return string == QString::fromLocal8Bit(string.toLocal8Bit());
}
void LauncherPartLaunch::executeTask() void LauncherPartLaunch::executeTask()
{ {
QString jarPath = APPLICATION->getJarPath("NewLaunch.jar"); QString jarPath = APPLICATION->getJarPath("NewLaunch.jar");
@ -136,24 +110,15 @@ void LauncherPartLaunch::executeTask()
auto natPath = minecraftInstance->getNativePath(); auto natPath = minecraftInstance->getNativePath();
#ifdef Q_OS_WIN #ifdef Q_OS_WIN
if (!fitsInLocal8bit(natPath)) { natPath = FS::getPathNameInLocal8bit(natPath);
args << "-Djava.library.path=" + shortPathName(natPath);
} else {
args << "-Djava.library.path=" + natPath;
}
#else
args << "-Djava.library.path=" + natPath;
#endif #endif
args << "-Djava.library.path=" + natPath;
args << "-cp"; args << "-cp";
#ifdef Q_OS_WIN #ifdef Q_OS_WIN
QStringList processed; QStringList processed;
for (auto& item : classPath) { for (auto& item : classPath) {
if (!fitsInLocal8bit(item)) { processed << FS::getPathNameInLocal8bit(item);
processed << shortPathName(item);
} else {
processed << item;
}
} }
args << processed.join(';'); args << processed.join(';');
#else #else

View File

@ -57,9 +57,11 @@ GetModDependenciesTask::GetModDependenciesTask(QObject* parent,
, m_version(mcVersion(instance)) , m_version(mcVersion(instance))
, m_loaderType(mcLoaders(instance)) , m_loaderType(mcLoaders(instance))
{ {
for (auto mod : folder->allMods()) for (auto mod : folder->allMods()) {
m_mods_file_names << mod->fileinfo().fileName();
if (auto meta = mod->metadata(); meta) if (auto meta = mod->metadata(); meta)
m_mods.append(meta); m_mods.append(meta);
}
prepare(); prepare();
} }
@ -183,7 +185,7 @@ Task::Ptr GetModDependenciesTask::prepareDependencyTask(const ModPlatform::Depen
ResourceAPI::DependencySearchArgs args = { dep, m_version, m_loaderType }; ResourceAPI::DependencySearchArgs args = { dep, m_version, m_loaderType };
ResourceAPI::DependencySearchCallbacks callbacks; ResourceAPI::DependencySearchCallbacks callbacks;
callbacks.on_fail = [](QString reason, int) { callbacks.on_fail = [](QString reason, int) {
qCritical() << tr("A network error occurred. Could not load project dependenies:%1").arg(reason); qCritical() << tr("A network error occurred. Could not load project dependencies:%1").arg(reason);
}; };
callbacks.on_succeed = [dep, provider, pDep, level, this](auto& doc, [[maybe_unused]] auto& pack) { callbacks.on_succeed = [dep, provider, pDep, level, this](auto& doc, [[maybe_unused]] auto& pack) {
try { try {
@ -231,8 +233,13 @@ Task::Ptr GetModDependenciesTask::prepareDependencyTask(const ModPlatform::Depen
if (dep_.addonId != pDep->version.addonId) { if (dep_.addonId != pDep->version.addonId) {
removePack(pDep->version.addonId); removePack(pDep->version.addonId);
addTask(prepareDependencyTask(dep_, provider.name, level)); addTask(prepareDependencyTask(dep_, provider.name, level));
} else } else {
addTask(getProjectInfoTask(pDep)); addTask(getProjectInfoTask(pDep));
}
}
if (isLocalyInstalled(pDep)) {
removePack(pDep->version.addonId);
return;
} }
for (auto dep_ : getDependenciesForVersion(pDep->version, provider.name)) { for (auto dep_ : getDependenciesForVersion(pDep->version, provider.name)) {
addTask(prepareDependencyTask(dep_, provider.name, level - 1)); addTask(prepareDependencyTask(dep_, provider.name, level - 1));
@ -258,9 +265,9 @@ void GetModDependenciesTask::removePack(const QVariant& addonId)
#endif #endif
} }
QHash<QString, QStringList> GetModDependenciesTask::getRequiredBy() auto GetModDependenciesTask::getExtraInfo() -> QHash<QString, PackDependencyExtraInfo>
{ {
QHash<QString, QStringList> rby; QHash<QString, PackDependencyExtraInfo> rby;
auto fullList = m_selected + m_pack_dependencies; auto fullList = m_selected + m_pack_dependencies;
for (auto& mod : fullList) { for (auto& mod : fullList) {
auto addonId = mod->pack->addonId; auto addonId = mod->pack->addonId;
@ -282,7 +289,61 @@ QHash<QString, QStringList> GetModDependenciesTask::getRequiredBy()
req.append(smod->pack->name); req.append(smod->pack->name);
} }
} }
rby[addonId.toString()] = req; rby[addonId.toString()] = { maybeInstalled(mod), req };
} }
return rby; return rby;
} }
// super lax compare (but not fuzzy)
// convert to lowercase
// convert all speratores to whitespace
// simplify sequence of internal whitespace to a single space
// efectivly compare two strings ignoring all separators and case
auto laxCompare = [](QString fsfilename, QString metadataFilename, bool excludeDigits = false) {
// allowed character seperators
QList<QChar> allowedSeperators = { '-', '+', '.', '_' };
if (excludeDigits)
allowedSeperators.append({ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' });
// copy in lowercase
auto fsName = fsfilename.toLower();
auto metaName = metadataFilename.toLower();
// replace all potential allowed seperatores with whitespace
for (auto sep : allowedSeperators) {
fsName = fsName.replace(sep, ' ');
metaName = metaName.replace(sep, ' ');
}
// remove extraneous whitespace
fsName = fsName.simplified();
metaName = metaName.simplified();
return fsName.compare(metaName) == 0;
};
bool GetModDependenciesTask::isLocalyInstalled(std::shared_ptr<PackDependency> pDep)
{
return pDep->version.fileName.isEmpty() ||
std::find_if(m_selected.begin(), m_selected.end(),
[pDep](std::shared_ptr<PackDependency> i) {
return !i->version.fileName.isEmpty() && laxCompare(i->version.fileName, pDep->version.fileName);
}) != m_selected.end() || // check the selected versions
std::find_if(m_mods_file_names.begin(), m_mods_file_names.end(),
[pDep](QString i) { return !i.isEmpty() && laxCompare(i, pDep->version.fileName); }) !=
m_mods_file_names.end() || // check the existing mods
std::find_if(m_pack_dependencies.begin(), m_pack_dependencies.end(), [pDep](std::shared_ptr<PackDependency> i) {
return pDep->pack->addonId != i->pack->addonId && !i->version.fileName.isEmpty() &&
laxCompare(pDep->version.fileName, i->version.fileName);
}) != m_pack_dependencies.end(); // check loaded dependencies
}
bool GetModDependenciesTask::maybeInstalled(std::shared_ptr<PackDependency> pDep)
{
return std::find_if(m_mods_file_names.begin(), m_mods_file_names.end(), [pDep](QString i) {
return !i.isEmpty() && laxCompare(i, pDep->version.fileName, true);
}) != m_mods_file_names.end(); // check the existing mods
}

View File

@ -50,6 +50,11 @@ class GetModDependenciesTask : public SequentialTask {
} }
}; };
struct PackDependencyExtraInfo {
bool maybe_installed;
QStringList required_by;
};
struct Provider { struct Provider {
ModPlatform::ResourceProvider name; ModPlatform::ResourceProvider name;
std::shared_ptr<ResourceDownload::ModModel> mod; std::shared_ptr<ResourceDownload::ModModel> mod;
@ -62,7 +67,7 @@ class GetModDependenciesTask : public SequentialTask {
QList<std::shared_ptr<PackDependency>> selected); QList<std::shared_ptr<PackDependency>> selected);
auto getDependecies() const -> QList<std::shared_ptr<PackDependency>> { return m_pack_dependencies; } auto getDependecies() const -> QList<std::shared_ptr<PackDependency>> { return m_pack_dependencies; }
QHash<QString, QStringList> getRequiredBy(); QHash<QString, PackDependencyExtraInfo> getExtraInfo();
protected slots: protected slots:
Task::Ptr prepareDependencyTask(const ModPlatform::Dependency&, ModPlatform::ResourceProvider, int); Task::Ptr prepareDependencyTask(const ModPlatform::Dependency&, ModPlatform::ResourceProvider, int);
@ -73,10 +78,14 @@ class GetModDependenciesTask : public SequentialTask {
ModPlatform::Dependency getOverride(const ModPlatform::Dependency&, ModPlatform::ResourceProvider providerName); ModPlatform::Dependency getOverride(const ModPlatform::Dependency&, ModPlatform::ResourceProvider providerName);
void removePack(const QVariant& addonId); void removePack(const QVariant& addonId);
bool isLocalyInstalled(std::shared_ptr<PackDependency> pDep);
bool maybeInstalled(std::shared_ptr<PackDependency> pDep);
private: private:
QList<std::shared_ptr<PackDependency>> m_pack_dependencies; QList<std::shared_ptr<PackDependency>> m_pack_dependencies;
QList<std::shared_ptr<Metadata::ModStruct>> m_mods; QList<std::shared_ptr<Metadata::ModStruct>> m_mods;
QList<std::shared_ptr<PackDependency>> m_selected; QList<std::shared_ptr<PackDependency>> m_selected;
QStringList m_mods_file_names;
Provider m_flame_provider; Provider m_flame_provider;
Provider m_modrinth_provider; Provider m_modrinth_provider;

View File

@ -469,7 +469,7 @@ bool processZIP(Mod& mod, [[maybe_unused]] ProcessingLevel level)
QuaZipFile file(&zip); QuaZipFile file(&zip);
if (zip.setCurrentFile("META-INF/mods.toml")) { if (zip.setCurrentFile("META-INF/mods.toml") || zip.setCurrentFile("META-INF/neoforge.mods.toml")) {
if (!file.open(QIODevice::ReadOnly)) { if (!file.open(QIODevice::ReadOnly)) {
zip.close(); zip.close();
return false; return false;

View File

@ -28,6 +28,7 @@ class CheckUpdateTask : public Task {
QString changelog; QString changelog;
ModPlatform::ResourceProvider provider; ModPlatform::ResourceProvider provider;
shared_qobject_ptr<ResourceDownloadTask> download; shared_qobject_ptr<ResourceDownloadTask> download;
bool enabled = true;
public: public:
UpdatableMod(QString name, UpdatableMod(QString name,
@ -37,7 +38,8 @@ class CheckUpdateTask : public Task {
std::optional<ModPlatform::IndexedVersionType> new_v_type, std::optional<ModPlatform::IndexedVersionType> new_v_type,
QString changelog, QString changelog,
ModPlatform::ResourceProvider p, ModPlatform::ResourceProvider p,
shared_qobject_ptr<ResourceDownloadTask> t) shared_qobject_ptr<ResourceDownloadTask> t,
bool enabled = true)
: name(name) : name(name)
, old_hash(old_h) , old_hash(old_h)
, old_version(old_v) , old_version(old_v)
@ -46,6 +48,7 @@ class CheckUpdateTask : public Task {
, changelog(changelog) , changelog(changelog)
, provider(p) , provider(p)
, download(t) , download(t)
, enabled(enabled)
{} {}
}; };

View File

@ -122,6 +122,8 @@ struct ExtraPackData {
QString wikiUrl; QString wikiUrl;
QString discordUrl; QString discordUrl;
QString status;
QString body; QString body;
}; };

View File

@ -354,6 +354,8 @@ bool FlameCreationTask::createInstance()
auto id = loader.id; auto id = loader.id;
if (id.startsWith("neoforge-")) { if (id.startsWith("neoforge-")) {
id.remove("neoforge-"); id.remove("neoforge-");
if (id.startsWith("1.20.1-"))
id.remove("1.20.1-"); // this is a mess for curseforge
loaderType = "neoforge"; loaderType = "neoforge";
loaderUid = "net.neoforged"; loaderUid = "net.neoforged";
} else if (id.startsWith("forge-")) { } else if (id.startsWith("forge-")) {

View File

@ -393,13 +393,17 @@ QByteArray FlamePackExportTask::generateIndex()
version["version"] = minecraft->m_version; version["version"] = minecraft->m_version;
QString id; QString id;
if (quilt != nullptr) if (quilt != nullptr)
id = "quilt-" + quilt->getVersion(); id = "quilt-" + quilt->m_version;
else if (fabric != nullptr) else if (fabric != nullptr)
id = "fabric-" + fabric->getVersion(); id = "fabric-" + fabric->m_version;
else if (forge != nullptr) else if (forge != nullptr)
id = "forge-" + forge->getVersion(); id = "forge-" + forge->m_version;
else if (neoforge != nullptr) else if (neoforge != nullptr) {
id = "neoforge-" + neoforge->getVersion(); id = "neoforge-";
if (minecraft->m_version == "1.20.1")
id += "1.20.1-";
id += neoforge->m_version;
}
version["modLoaders"] = QJsonArray(); version["modLoaders"] = QJsonArray();
if (!id.isEmpty()) { if (!id.isEmpty()) {
QJsonObject loader; QJsonObject loader;

View File

@ -43,7 +43,7 @@ Task::Ptr NetworkResourceAPI::searchProjects(SearchArgs&& args, SearchCallbacks&
callbacks.on_succeed(doc); callbacks.on_succeed(doc);
}); });
QObject::connect(netJob.get(), &NetJob::failed, [&netJob, callbacks](QString reason) { QObject::connect(netJob.get(), &NetJob::failed, [netJob, callbacks](const QString& reason) {
int network_error_code = -1; int network_error_code = -1;
if (auto* failed_action = netJob->getFailedActions().at(0); failed_action && failed_action->m_reply) if (auto* failed_action = netJob->getFailedActions().at(0); failed_action && failed_action->m_reply)
network_error_code = failed_action->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); network_error_code = failed_action->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
@ -102,7 +102,7 @@ Task::Ptr NetworkResourceAPI::getProjectVersions(VersionSearchArgs&& args, Versi
callbacks.on_succeed(doc, args.pack); callbacks.on_succeed(doc, args.pack);
}); });
QObject::connect(netJob.get(), &NetJob::failed, [&netJob, callbacks](QString reason) { QObject::connect(netJob.get(), &NetJob::failed, [netJob, callbacks](const QString& reason) {
int network_error_code = -1; int network_error_code = -1;
if (auto* failed_action = netJob->getFailedActions().at(0); failed_action && failed_action->m_reply) if (auto* failed_action = netJob->getFailedActions().at(0); failed_action && failed_action->m_reply)
network_error_code = failed_action->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); network_error_code = failed_action->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
@ -153,7 +153,7 @@ Task::Ptr NetworkResourceAPI::getDependencyVersion(DependencySearchArgs&& args,
callbacks.on_succeed(doc, args.dependency); callbacks.on_succeed(doc, args.dependency);
}); });
QObject::connect(netJob.get(), &NetJob::failed, [&netJob, callbacks](QString reason) { QObject::connect(netJob.get(), &NetJob::failed, [netJob, callbacks](const QString& reason) {
int network_error_code = -1; int network_error_code = -1;
if (auto* failed_action = netJob->getFailedActions().at(0); failed_action && failed_action->m_reply) if (auto* failed_action = netJob->getFailedActions().at(0); failed_action && failed_action->m_reply)
network_error_code = failed_action->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); network_error_code = failed_action->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();

View File

@ -43,6 +43,7 @@ Modpack parseDirectory(QString path)
modpack.version = Json::requireString(root, "version", "version"); modpack.version = Json::requireString(root, "version", "version");
modpack.mcVersion = Json::requireString(root, "mcVersion", "mcVersion"); modpack.mcVersion = Json::requireString(root, "mcVersion", "mcVersion");
modpack.jvmArgs = Json::ensureVariant(root, "jvmArgs", {}, "jvmArgs"); modpack.jvmArgs = Json::ensureVariant(root, "jvmArgs", {}, "jvmArgs");
modpack.totalPlayTime = Json::requireInteger(root, "totalPlayTime", "totalPlayTime");
} catch (const Exception& e) { } catch (const Exception& e) {
qDebug() << "Couldn't load ftb instance json: " << e.cause(); qDebug() << "Couldn't load ftb instance json: " << e.cause();
return {}; return {};

View File

@ -36,6 +36,7 @@ struct Modpack {
QString name; QString name;
QString version; QString version;
QString mcVersion; QString mcVersion;
int totalPlayTime;
// not needed for instance creation // not needed for instance creation
QVariant jvmArgs; QVariant jvmArgs;

View File

@ -37,7 +37,7 @@ void PackInstallTask::executeTask()
progress(1, 2); progress(1, 2);
m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this] { m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this] {
FS::copy folderCopy(m_pack.path, FS::PathCombine(m_stagingPath, ".minecraft")); FS::copy folderCopy(m_pack.path, FS::PathCombine(m_stagingPath, "minecraft"));
folderCopy.followSymlinks(true); folderCopy.followSymlinks(true);
return folderCopy(); return folderCopy();
}); });
@ -55,6 +55,7 @@ void PackInstallTask::copySettings()
instanceSettings->suspendSave(); instanceSettings->suspendSave();
MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath); MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath);
instance.settings()->set("InstanceType", "OneSix"); instance.settings()->set("InstanceType", "OneSix");
instance.settings()->set("totalTimePlayed", m_pack.totalPlayTime / 1000);
if (m_pack.jvmArgs.isValid() && !m_pack.jvmArgs.toString().isEmpty()) { if (m_pack.jvmArgs.isValid() && !m_pack.jvmArgs.toString().isEmpty()) {
instance.settings()->set("OverrideJavaArgs", true); instance.settings()->set("OverrideJavaArgs", true);

View File

@ -137,7 +137,7 @@ void PackInstallTask::install()
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;
} }
@ -155,7 +155,7 @@ void PackInstallTask::install()
bool fallback = true; bool fallback = true;
// 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);

View File

@ -20,6 +20,7 @@
#include "ui/pages/modplatform/OptionalModDialog.h" #include "ui/pages/modplatform/OptionalModDialog.h"
#include <QAbstractButton> #include <QAbstractButton>
#include <QFileInfo>
#include <vector> #include <vector>
bool ModrinthCreationTask::abort() bool ModrinthCreationTask::abort()
@ -58,6 +59,7 @@ bool ModrinthCreationTask::updateInstance()
return false; return false;
auto version_name = inst->getManagedPackVersionName(); auto version_name = inst->getManagedPackVersionName();
m_root_path = QFileInfo(inst->gameRoot()).fileName();
auto version_str = !version_name.isEmpty() ? tr(" (version %1)").arg(version_name) : ""; auto version_str = !version_name.isEmpty() ? tr(" (version %1)").arg(version_name) : "";
if (shouldConfirmUpdate()) { if (shouldConfirmUpdate()) {
@ -173,7 +175,7 @@ bool ModrinthCreationTask::createInstance()
FS::ensureFilePathExists(new_index_place); FS::ensureFilePathExists(new_index_place);
QFile::rename(index_path, new_index_place); QFile::rename(index_path, new_index_place);
auto mcPath = FS::PathCombine(m_stagingPath, ".minecraft"); auto mcPath = FS::PathCombine(m_stagingPath, m_root_path);
auto override_path = FS::PathCombine(m_stagingPath, "overrides"); auto override_path = FS::PathCombine(m_stagingPath, "overrides");
if (QFile::exists(override_path)) { if (QFile::exists(override_path)) {
@ -234,7 +236,7 @@ bool ModrinthCreationTask::createInstance()
m_files_job.reset(new NetJob(tr("Mod Download Modrinth"), APPLICATION->network())); m_files_job.reset(new NetJob(tr("Mod Download Modrinth"), APPLICATION->network()));
auto root_modpack_path = FS::PathCombine(m_stagingPath, ".minecraft"); auto root_modpack_path = FS::PathCombine(m_stagingPath, m_root_path);
auto root_modpack_url = QUrl::fromLocalFile(root_modpack_path); auto root_modpack_url = QUrl::fromLocalFile(root_modpack_path);
for (auto file : m_files) { for (auto file : m_files) {

View File

@ -46,4 +46,6 @@ class ModrinthCreationTask final : public InstanceCreationTask {
NetJob::Ptr m_files_job; NetJob::Ptr m_files_job;
std::optional<InstancePtr> m_instance; std::optional<InstancePtr> m_instance;
QString m_root_path = "minecraft";
}; };

View File

@ -287,16 +287,12 @@ QByteArray ModrinthPackExportTask::generateIndex()
env["client"] = "required"; env["client"] = "required";
env["server"] = "required"; env["server"] = "required";
} }
switch (iterator->side) {
case Metadata::ModSide::ClientSide: // a server side mod does not imply that the mod does not work on the client
env["server"] = "unsupported"; // however, if a mrpack mod is marked as server-only it will not install on the client
break; if (iterator->side == Metadata::ModSide::ClientSide)
case Metadata::ModSide::ServerSide: env["server"] = "unsupported";
env["client"] = "unsupported";
break;
case Metadata::ModSide::UniversalSide:
break;
}
fileOut["env"] = env; fileOut["env"] = env;
fileOut["path"] = path; fileOut["path"] = path;

View File

@ -104,6 +104,8 @@ void Modrinth::loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& ob
pack.extraData.donate.append(donate); pack.extraData.donate.append(donate);
} }
pack.extraData.status = Json::ensureString(obj, "status");
pack.extraData.body = Json::ensureString(obj, "body").remove("<br>"); pack.extraData.body = Json::ensureString(obj, "body").remove("<br>");
pack.extraDataLoaded = true; pack.extraDataLoaded = true;

View File

@ -95,6 +95,8 @@ void loadIndexedInfo(Modpack& pack, QJsonObject& obj)
pack.extra.donate.append(donate); pack.extra.donate.append(donate);
} }
pack.extra.status = Json::ensureString(obj, "status");
pack.extraInfoLoaded = true; pack.extraInfoLoaded = true;
} }

View File

@ -77,6 +77,8 @@ struct ModpackExtra {
QString discordUrl; QString discordUrl;
QList<DonationData> donate; QList<DonationData> donate;
QString status;
}; };
struct ModpackVersion { struct ModpackVersion {

View File

@ -62,7 +62,7 @@ void Technic::SingleZipPackInstallTask::downloadSucceeded()
m_abortable = false; m_abortable = false;
setStatus(tr("Extracting modpack")); setStatus(tr("Extracting modpack"));
QDir extractDir(FS::PathCombine(m_stagingPath, ".minecraft")); QDir extractDir(FS::PathCombine(m_stagingPath, "minecraft"));
qDebug() << "Attempting to create instance from" << m_archivePath; qDebug() << "Attempting to create instance from" << m_archivePath;
// open the zip and find relevant files in it // open the zip and find relevant files in it

View File

@ -140,7 +140,7 @@ void Technic::SolderPackInstallTask::downloadSucceeded()
m_filesNetJob.reset(); m_filesNetJob.reset();
m_extractFuture = QtConcurrent::run([this]() { m_extractFuture = QtConcurrent::run([this]() {
int i = 0; int i = 0;
QString extractDir = FS::PathCombine(m_stagingPath, ".minecraft"); QString extractDir = FS::PathCombine(m_stagingPath, "minecraft");
FS::ensureFolderPathExists(extractDir); FS::ensureFolderPathExists(extractDir);
while (m_modCount > i) { while (m_modCount > i) {

View File

@ -33,7 +33,7 @@ void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings,
const QString& minecraftVersion, const QString& minecraftVersion,
[[maybe_unused]] const bool isSolder) [[maybe_unused]] const bool isSolder)
{ {
QString minecraftPath = FS::PathCombine(stagingPath, ".minecraft"); QString minecraftPath = FS::PathCombine(stagingPath, "minecraft");
QString configPath = FS::PathCombine(stagingPath, "instance.cfg"); QString configPath = FS::PathCombine(stagingPath, "instance.cfg");
auto instanceSettings = std::make_shared<INISettingsObject>(configPath); auto instanceSettings = std::make_shared<INISettingsObject>(configPath);
MinecraftInstance instance(globalSettings, instanceSettings, stagingPath); MinecraftInstance instance(globalSettings, instanceSettings, stagingPath);

View File

@ -5,6 +5,7 @@
qt.*.debug=false qt.*.debug=false
# don't log credentials by default # don't log credentials by default
launcher.auth.credentials.debug=false launcher.auth.credentials.debug=false
katabasis.*.debug=false
# remove the debug lines, other log levels still get through # remove the debug lines, other log levels still get through
launcher.task.net.download.debug=false launcher.task.net.download.debug=false
# enable or disable whole catageries # enable or disable whole catageries

View File

@ -1195,43 +1195,43 @@ void MainWindow::undoTrashInstance()
void MainWindow::on_actionViewLauncherRootFolder_triggered() void MainWindow::on_actionViewLauncherRootFolder_triggered()
{ {
DesktopServices::openDirectory("."); DesktopServices::openPath(".");
} }
void MainWindow::on_actionViewInstanceFolder_triggered() void MainWindow::on_actionViewInstanceFolder_triggered()
{ {
QString str = APPLICATION->settings()->get("InstanceDir").toString(); QString str = APPLICATION->settings()->get("InstanceDir").toString();
DesktopServices::openDirectory(str); DesktopServices::openPath(str);
} }
void MainWindow::on_actionViewCentralModsFolder_triggered() void MainWindow::on_actionViewCentralModsFolder_triggered()
{ {
DesktopServices::openDirectory(APPLICATION->settings()->get("CentralModsDir").toString(), true); DesktopServices::openPath(APPLICATION->settings()->get("CentralModsDir").toString(), true);
} }
void MainWindow::on_actionViewIconThemeFolder_triggered() void MainWindow::on_actionViewIconThemeFolder_triggered()
{ {
DesktopServices::openDirectory(APPLICATION->themeManager()->getIconThemesFolder().path(), true); DesktopServices::openPath(APPLICATION->themeManager()->getIconThemesFolder().path(), true);
} }
void MainWindow::on_actionViewWidgetThemeFolder_triggered() void MainWindow::on_actionViewWidgetThemeFolder_triggered()
{ {
DesktopServices::openDirectory(APPLICATION->themeManager()->getApplicationThemesFolder().path(), true); DesktopServices::openPath(APPLICATION->themeManager()->getApplicationThemesFolder().path(), true);
} }
void MainWindow::on_actionViewCatPackFolder_triggered() void MainWindow::on_actionViewCatPackFolder_triggered()
{ {
DesktopServices::openDirectory(APPLICATION->themeManager()->getCatPacksFolder().path(), true); DesktopServices::openPath(APPLICATION->themeManager()->getCatPacksFolder().path(), true);
} }
void MainWindow::on_actionViewIconsFolder_triggered() void MainWindow::on_actionViewIconsFolder_triggered()
{ {
DesktopServices::openDirectory(APPLICATION->icons()->getDirectory(), true); DesktopServices::openPath(APPLICATION->icons()->getDirectory(), true);
} }
void MainWindow::on_actionViewLogsFolder_triggered() void MainWindow::on_actionViewLogsFolder_triggered()
{ {
DesktopServices::openDirectory("logs", true); DesktopServices::openPath("logs", true);
} }
void MainWindow::refreshInstances() void MainWindow::refreshInstances()
@ -1442,7 +1442,7 @@ 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::openPath(QFileInfo(str));
} }
} }

View File

@ -174,8 +174,7 @@ AboutDialog::AboutDialog(QWidget* parent) : QDialog(parent), ui(new Ui::AboutDia
QString urlText("<html><head/><body><p><a href=\"%1\">%1</a></p></body></html>"); QString urlText("<html><head/><body><p><a href=\"%1\">%1</a></p></body></html>");
ui->urlLabel->setText(urlText.arg(BuildConfig.LAUNCHER_GIT)); ui->urlLabel->setText(urlText.arg(BuildConfig.LAUNCHER_GIT));
QString copyText("© 2022-2023 %1"); ui->copyLabel->setText(BuildConfig.LAUNCHER_COPYRIGHT);
ui->copyLabel->setText(copyText.arg(BuildConfig.LAUNCHER_COPYRIGHT));
connect(ui->closeButton, SIGNAL(clicked()), SLOT(close())); connect(ui->closeButton, SIGNAL(clicked()), SLOT(close()));

View File

@ -47,11 +47,18 @@ ExportPackDialog::ExportPackDialog(InstancePtr instance, QWidget* parent, ModPla
if (m_provider == ModPlatform::ResourceProvider::MODRINTH) { if (m_provider == ModPlatform::ResourceProvider::MODRINTH) {
setWindowTitle(tr("Export Modrinth Pack")); setWindowTitle(tr("Export Modrinth Pack"));
ui->summary->setText(instance->settings()->get("ExportSummary").toString());
ui->authorLabel->hide();
ui->author->hide();
ui->summary->setPlainText(instance->settings()->get("ExportSummary").toString());
} else { } else {
setWindowTitle(tr("Export CurseForge Pack")); setWindowTitle(tr("Export CurseForge Pack"));
ui->summaryLabel->setText(tr("&Author"));
ui->summary->setText(instance->settings()->get("ExportAuthor").toString()); ui->summaryLabel->hide();
ui->summary->hide();
ui->author->setText(instance->settings()->get("ExportAuthor").toString());
} }
// ensure a valid pack is generated // ensure a valid pack is generated
@ -108,9 +115,13 @@ void ExportPackDialog::done(int result)
auto settings = instance->settings(); auto settings = instance->settings();
settings->set("ExportName", ui->name->text()); settings->set("ExportName", ui->name->text());
settings->set("ExportVersion", ui->version->text()); settings->set("ExportVersion", ui->version->text());
settings->set(m_provider == ModPlatform::ResourceProvider::FLAME ? "ExportAuthor" : "ExportSummary", ui->summary->text());
settings->set("ExportOptionalFiles", ui->optionalFiles->isChecked()); settings->set("ExportOptionalFiles", ui->optionalFiles->isChecked());
if (m_provider == ModPlatform::ResourceProvider::MODRINTH)
settings->set("ExportSummary", ui->summary->toPlainText());
else
settings->set("ExportAuthor", ui->author->text());
if (result == Accepted) { if (result == Accepted) {
const QString name = ui->name->text().isEmpty() ? instance->name() : ui->name->text(); const QString name = ui->name->text().isEmpty() ? instance->name() : ui->name->text();
const QString filename = FS::RemoveInvalidFilenameChars(name); const QString filename = FS::RemoveInvalidFilenameChars(name);
@ -134,10 +145,10 @@ void ExportPackDialog::done(int result)
Task* task; Task* task;
if (m_provider == ModPlatform::ResourceProvider::MODRINTH) { if (m_provider == ModPlatform::ResourceProvider::MODRINTH) {
task = new ModrinthPackExportTask(name, ui->version->text(), ui->summary->text(), ui->optionalFiles->isChecked(), instance, task = new ModrinthPackExportTask(name, ui->version->text(), ui->summary->toPlainText(), ui->optionalFiles->isChecked(),
output, std::bind(&FileIgnoreProxy::filterFile, proxy, std::placeholders::_1)); instance, output, std::bind(&FileIgnoreProxy::filterFile, proxy, std::placeholders::_1));
} else { } else {
task = new FlamePackExportTask(name, ui->version->text(), ui->summary->text(), ui->optionalFiles->isChecked(), instance, output, task = new FlamePackExportTask(name, ui->version->text(), ui->author->text(), ui->optionalFiles->isChecked(), instance, output,
std::bind(&FileIgnoreProxy::filterFile, proxy, std::placeholders::_1)); std::bind(&FileIgnoreProxy::filterFile, proxy, std::placeholders::_1));
} }

View File

@ -7,7 +7,7 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>650</width> <width>650</width>
<height>510</height> <height>532</height>
</rect> </rect>
</property> </property>
<property name="sizeGripEnabled"> <property name="sizeGripEnabled">
@ -19,21 +19,8 @@
<property name="title"> <property name="title">
<string>&amp;Description</string> <string>&amp;Description</string>
</property> </property>
<layout class="QGridLayout" name="gridLayout"> <layout class="QVBoxLayout" name="verticalLayout_3">
<item row="3" column="0"> <item>
<widget class="QLabel" name="summaryLabel">
<property name="text">
<string>&amp;Summary</string>
</property>
<property name="buddy">
<cstring>summary</cstring>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="summary"/>
</item>
<item row="0" column="0">
<widget class="QLabel" name="nameLabel"> <widget class="QLabel" name="nameLabel">
<property name="text"> <property name="text">
<string>&amp;Name</string> <string>&amp;Name</string>
@ -43,7 +30,10 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="0"> <item>
<widget class="QLineEdit" name="name"/>
</item>
<item>
<widget class="QLabel" name="versionLabel"> <widget class="QLabel" name="versionLabel">
<property name="text"> <property name="text">
<string>&amp;Version</string> <string>&amp;Version</string>
@ -53,16 +43,43 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="0" column="1"> <item>
<widget class="QLineEdit" name="name"/>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="version"> <widget class="QLineEdit" name="version">
<property name="text"> <property name="text">
<string>1.0.0</string> <string>1.0.0</string>
</property> </property>
</widget> </widget>
</item> </item>
<item>
<widget class="QLabel" name="summaryLabel">
<property name="text">
<string>&amp;Summary</string>
</property>
<property name="buddy">
<cstring>summary</cstring>
</property>
</widget>
</item>
<item>
<widget class="QPlainTextEdit" name="summary">
<property name="tabChangesFocus">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="authorLabel">
<property name="text">
<string>&amp;Author</string>
</property>
<property name="buddy">
<cstring>author</cstring>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="author"/>
</item>
</layout> </layout>
</widget> </widget>
</item> </item>
@ -124,6 +141,7 @@
<tabstop>name</tabstop> <tabstop>name</tabstop>
<tabstop>version</tabstop> <tabstop>version</tabstop>
<tabstop>summary</tabstop> <tabstop>summary</tabstop>
<tabstop>author</tabstop>
<tabstop>files</tabstop> <tabstop>files</tabstop>
<tabstop>optionalFiles</tabstop> <tabstop>optionalFiles</tabstop>
</tabstops> </tabstops>

View File

@ -159,5 +159,5 @@ IconPickerDialog::~IconPickerDialog()
void IconPickerDialog::openFolder() void IconPickerDialog::openFolder()
{ {
DesktopServices::openDirectory(APPLICATION->icons()->getDirectory(), true); DesktopServices::openPath(APPLICATION->icons()->getDirectory(), true);
} }

View File

@ -214,19 +214,25 @@ void ModUpdateDialog::checkCandidates()
} }
static FlameAPI api; static FlameAPI api;
auto getRequiredBy = depTask->getRequiredBy(); auto dependencyExtraInfo = depTask->getExtraInfo();
for (auto dep : depTask->getDependecies()) { for (auto dep : depTask->getDependecies()) {
auto changelog = dep->version.changelog; auto changelog = dep->version.changelog;
if (dep->pack->provider == ModPlatform::ResourceProvider::FLAME) if (dep->pack->provider == ModPlatform::ResourceProvider::FLAME)
changelog = api.getModFileChangelog(dep->version.addonId.toInt(), dep->version.fileId.toInt()); changelog = api.getModFileChangelog(dep->version.addonId.toInt(), dep->version.fileId.toInt());
auto download_task = makeShared<ResourceDownloadTask>(dep->pack, dep->version, m_mod_model); auto download_task = makeShared<ResourceDownloadTask>(dep->pack, dep->version, m_mod_model);
CheckUpdateTask::UpdatableMod updatable = { auto extraInfo = dependencyExtraInfo.value(dep->version.addonId.toString());
dep->pack->name, dep->version.hash, "", dep->version.version, dep->version.version_type, CheckUpdateTask::UpdatableMod updatable = { dep->pack->name,
changelog, dep->pack->provider, download_task dep->version.hash,
}; "",
dep->version.version,
dep->version.version_type,
changelog,
dep->pack->provider,
download_task,
!extraInfo.maybe_installed };
appendMod(updatable, getRequiredBy.value(dep->version.addonId.toString())); appendMod(updatable, extraInfo.required_by);
m_tasks.insert(updatable.name, updatable.download); m_tasks.insert(updatable.name, updatable.download);
} }
} }
@ -412,7 +418,10 @@ void ModUpdateDialog::onMetadataFailed(Mod* mod, bool try_others, ModPlatform::R
void ModUpdateDialog::appendMod(CheckUpdateTask::UpdatableMod const& info, QStringList requiredBy) void ModUpdateDialog::appendMod(CheckUpdateTask::UpdatableMod const& info, QStringList requiredBy)
{ {
auto item_top = new QTreeWidgetItem(ui->modTreeWidget); auto item_top = new QTreeWidgetItem(ui->modTreeWidget);
item_top->setCheckState(0, Qt::CheckState::Checked); item_top->setCheckState(0, info.enabled ? Qt::CheckState::Checked : Qt::CheckState::Unchecked);
if (!info.enabled) {
item_top->setToolTip(0, tr("Mod was disabled as it may be already instaled."));
}
item_top->setText(0, info.name); item_top->setText(0, info.name);
item_top->setExpanded(true); item_top->setExpanded(true);
@ -443,6 +452,9 @@ void ModUpdateDialog::appendMod(CheckUpdateTask::UpdatableMod const& info, QStri
reqItem->insertChildren(i++, { reqItem }); reqItem->insertChildren(i++, { reqItem });
} }
} }
ui->toggleDepsButton->show();
m_deps << item_top;
} }
auto changelog_item = new QTreeWidgetItem(item_top); auto changelog_item = new QTreeWidgetItem(item_top);

View File

@ -97,6 +97,9 @@ NewInstanceDialog::NewInstanceDialog(const QString& initialGroup,
ui->verticalLayout->insertWidget(2, m_container); ui->verticalLayout->insertWidget(2, m_container);
m_container->addButtons(m_buttons); m_container->addButtons(m_buttons);
connect(m_container, &PageContainer::selectedPageChanged, this, [this](BasePage* previous, BasePage* selected) {
m_buttons->button(QDialogButtonBox::Ok)->setEnabled(creationTask && !instName().isEmpty());
});
// Bonk Qt over its stupid head and make sure it understands which button is the default one... // Bonk Qt over its stupid head and make sure it understands which button is the default one...
// See: https://stackoverflow.com/questions/24556831/qbuttonbox-set-default-button // See: https://stackoverflow.com/questions/24556831/qbuttonbox-set-default-button

View File

@ -132,7 +132,7 @@ void ResourceDownloadDialog::confirm()
auto confirm_dialog = ReviewMessageBox::create(this, tr("Confirm %1 to download").arg(resourcesString())); auto confirm_dialog = ReviewMessageBox::create(this, tr("Confirm %1 to download").arg(resourcesString()));
confirm_dialog->retranslateUi(resourcesString()); confirm_dialog->retranslateUi(resourcesString());
QHash<QString, QStringList> getRequiredBy; QHash<QString, GetModDependenciesTask::PackDependencyExtraInfo> dependencyExtraInfo;
if (auto task = getModDependenciesTask(); task) { if (auto task = getModDependenciesTask(); task) {
connect(task.get(), &Task::failed, this, connect(task.get(), &Task::failed, this,
[&](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); [&](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); });
@ -157,7 +157,7 @@ void ResourceDownloadDialog::confirm()
} else { } else {
for (auto dep : task->getDependecies()) for (auto dep : task->getDependecies())
addResource(dep->pack, dep->version); addResource(dep->pack, dep->version);
getRequiredBy = task->getRequiredBy(); dependencyExtraInfo = task->getExtraInfo();
} }
} }
@ -166,9 +166,10 @@ void ResourceDownloadDialog::confirm()
return QString::compare(a->getName(), b->getName(), Qt::CaseInsensitive) < 0; return QString::compare(a->getName(), b->getName(), Qt::CaseInsensitive) < 0;
}); });
for (auto& task : selected) { for (auto& task : selected) {
auto extraInfo = dependencyExtraInfo.value(task->getPack()->addonId.toString());
confirm_dialog->appendResource({ task->getName(), task->getFilename(), task->getCustomPath(), confirm_dialog->appendResource({ task->getName(), task->getFilename(), task->getCustomPath(),
ProviderCaps.name(task->getProvider()), getRequiredBy.value(task->getPack()->addonId.toString()), ProviderCaps.name(task->getProvider()), extraInfo.required_by,
task->getVersion().version_type.toString() }); task->getVersion().version_type.toString(), !extraInfo.maybe_installed });
} }
if (confirm_dialog->exec()) { if (confirm_dialog->exec()) {

View File

@ -13,6 +13,7 @@ ReviewMessageBox::ReviewMessageBox(QWidget* parent, [[maybe_unused]] QString con
auto back_button = ui->buttonBox->button(QDialogButtonBox::Cancel); auto back_button = ui->buttonBox->button(QDialogButtonBox::Cancel);
back_button->setText(tr("Back")); back_button->setText(tr("Back"));
ui->toggleDepsButton->hide();
ui->modTreeWidget->header()->setSectionResizeMode(0, QHeaderView::Stretch); ui->modTreeWidget->header()->setSectionResizeMode(0, QHeaderView::Stretch);
ui->modTreeWidget->header()->setStretchLastSection(false); ui->modTreeWidget->header()->setStretchLastSection(false);
ui->modTreeWidget->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents); ui->modTreeWidget->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
@ -34,8 +35,11 @@ auto ReviewMessageBox::create(QWidget* parent, QString&& title, QString&& icon)
void ReviewMessageBox::appendResource(ResourceInformation&& info) void ReviewMessageBox::appendResource(ResourceInformation&& info)
{ {
auto itemTop = new QTreeWidgetItem(ui->modTreeWidget); auto itemTop = new QTreeWidgetItem(ui->modTreeWidget);
itemTop->setCheckState(0, Qt::CheckState::Checked); itemTop->setCheckState(0, info.enabled ? Qt::CheckState::Checked : Qt::CheckState::Unchecked);
itemTop->setText(0, info.name); itemTop->setText(0, info.name);
if (!info.enabled) {
itemTop->setToolTip(0, tr("Mod was disabled as it may be already instaled."));
}
auto filenameItem = new QTreeWidgetItem(itemTop); auto filenameItem = new QTreeWidgetItem(itemTop);
filenameItem->setText(0, tr("Filename: %1").arg(info.filename)); filenameItem->setText(0, tr("Filename: %1").arg(info.filename));
@ -75,6 +79,8 @@ void ReviewMessageBox::appendResource(ResourceInformation&& info)
} }
itemTop->insertChildren(childIndx++, { requiredByItem }); itemTop->insertChildren(childIndx++, { requiredByItem });
ui->toggleDepsButton->show();
m_deps << itemTop;
} }
auto versionTypeItem = new QTreeWidgetItem(itemTop); auto versionTypeItem = new QTreeWidgetItem(itemTop);
@ -108,3 +114,10 @@ void ReviewMessageBox::retranslateUi(QString resources_name)
ui->explainLabel->setText(tr("You're about to download the following %1:").arg(resources_name)); ui->explainLabel->setText(tr("You're about to download the following %1:").arg(resources_name));
ui->onlyCheckedLabel->setText(tr("Only %1 with a check will be downloaded!").arg(resources_name)); ui->onlyCheckedLabel->setText(tr("Only %1 with a check will be downloaded!").arg(resources_name));
} }
void ReviewMessageBox::on_toggleDepsButton_clicked()
{
m_deps_checked = !m_deps_checked;
auto state = m_deps_checked ? Qt::Checked : Qt::Unchecked;
for (auto dep : m_deps)
dep->setCheckState(0, state);
};

View File

@ -1,6 +1,7 @@
#pragma once #pragma once
#include <QDialog> #include <QDialog>
#include <QTreeWidgetItem>
namespace Ui { namespace Ui {
class ReviewMessageBox; class ReviewMessageBox;
@ -19,6 +20,7 @@ class ReviewMessageBox : public QDialog {
QString provider; QString provider;
QStringList required_by; QStringList required_by;
QString version_type; QString version_type;
bool enabled = true;
}; };
void appendResource(ResourceInformation&& info); void appendResource(ResourceInformation&& info);
@ -28,8 +30,14 @@ class ReviewMessageBox : public QDialog {
~ReviewMessageBox() override; ~ReviewMessageBox() override;
protected slots:
void on_toggleDepsButton_clicked();
protected: protected:
ReviewMessageBox(QWidget* parent, const QString& title, const QString& icon); ReviewMessageBox(QWidget* parent, const QString& title, const QString& icon);
Ui::ReviewMessageBox* ui; Ui::ReviewMessageBox* ui;
QList<QTreeWidgetItem*> m_deps;
bool m_deps_checked = true;
}; };

View File

@ -44,15 +44,20 @@
</widget> </widget>
</item> </item>
<item row="1" column="0"> <item row="1" column="0">
<widget class="QLabel" name="explainLabel"> <widget class="QLabel" name="explainLabel"/>
</widget>
</item> </item>
<item row="5" column="0" rowspan="2"> <item row="5" column="0" rowspan="2">
<layout class="QHBoxLayout" name="horizontalLayout"> <layout class="QHBoxLayout" name="horizontalLayout">
<item> <item>
<widget class="QLabel" name="onlyCheckedLabel"> <widget class="QPushButton" name="toggleDepsButton">
<property name="text">
<string>Toggle Dependencies</string>
</property>
</widget> </widget>
</item> </item>
<item>
<widget class="QLabel" name="onlyCheckedLabel"/>
</item>
<item> <item>
<widget class="QDialogButtonBox" name="buttonBox"> <widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons"> <property name="standardButtons">

View File

@ -458,16 +458,18 @@ void InstanceView::paintEvent([[maybe_unused]] QPaintEvent* event)
QPainter painter(this->viewport()); QPainter painter(this->viewport());
if (m_catVisible) { if (m_catVisible) {
painter.setOpacity(APPLICATION->settings()->get("CatOpacity").toFloat() / 100);
int widWidth = this->viewport()->width(); int widWidth = this->viewport()->width();
int widHeight = this->viewport()->height(); int widHeight = this->viewport()->height();
if (m_catPixmap.width() < widWidth) if (m_catPixmap.width() < widWidth)
widWidth = m_catPixmap.width(); widWidth = m_catPixmap.width();
if (m_catPixmap.height() < widHeight) if (m_catPixmap.height() < widHeight)
widHeight = m_catPixmap.height(); widHeight = m_catPixmap.height();
auto pixmap = m_catPixmap.scaled(widWidth, widHeight, Qt::KeepAspectRatio); auto pixmap = m_catPixmap.scaled(widWidth, widHeight, Qt::KeepAspectRatio, Qt::SmoothTransformation);
QRect rectOfPixmap = pixmap.rect(); QRect rectOfPixmap = pixmap.rect();
rectOfPixmap.moveBottomRight(this->viewport()->rect().bottomRight()); rectOfPixmap.moveBottomRight(this->viewport()->rect().bottomRight());
painter.drawPixmap(rectOfPixmap.topLeft(), pixmap); painter.drawPixmap(rectOfPixmap.topLeft(), pixmap);
painter.setOpacity(1.0);
} }
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
@ -480,32 +482,42 @@ void InstanceView::paintEvent([[maybe_unused]] QPaintEvent* event)
if (model()->rowCount() == 0) { if (model()->rowCount() == 0) {
painter.save(); painter.save();
const QString line1 = tr("Welcome!"); QString emptyString = tr("Welcome!") + "\n" + tr("Click \"Add Instance\" to get started.");
const QString line2 = tr("Click \"Add Instance\" to get started.");
auto rect = this->viewport()->rect();
auto font = option.font;
font.setPointSize(37);
painter.setFont(font);
auto fm = painter.fontMetrics();
if (rect.height() <= (fm.height() * 5) || rect.width() <= fm.horizontalAdvance(line2)) { // calculate the rect for the overlay
auto s = rect.height() / (5. * fm.height()); painter.setRenderHint(QPainter::Antialiasing, true);
auto sx = rect.width() * 1. / fm.horizontalAdvance(line2); QFont font("sans", 20);
if (s >= sx) font.setBold(true);
s = sx;
auto ps = font.pointSize() * s; QRect bounds = viewport()->geometry();
if (ps <= 0) bounds.moveTop(0);
ps = 1; auto innerBounds = bounds;
font.setPointSize(ps); innerBounds.adjust(10, 10, -10, -10);
painter.setFont(font);
fm = painter.fontMetrics(); QColor background = QApplication::palette().color(QPalette::WindowText);
QColor foreground = QApplication::palette().color(QPalette::Base);
foreground.setAlpha(190);
painter.setFont(font);
auto fontMetrics = painter.fontMetrics();
auto textRect = fontMetrics.boundingRect(innerBounds, Qt::AlignHCenter | Qt::TextWordWrap, emptyString);
textRect.moveCenter(bounds.center());
auto wrapRect = textRect;
wrapRect.adjust(-10, -10, 10, 10);
// check if we are allowed to draw in our area
if (!event->rect().intersects(wrapRect)) {
return;
} }
// text painter.setBrush(QBrush(background));
rect.setTop(rect.top() + fm.height() * 1.5); painter.setPen(foreground);
painter.drawText(rect, Qt::AlignHCenter, line1); painter.drawRoundedRect(wrapRect, 5.0, 5.0);
rect.setTop(rect.top() + fm.height());
painter.drawText(rect, Qt::AlignHCenter, line2); painter.setPen(foreground);
painter.setFont(font);
painter.drawText(textRect, Qt::AlignHCenter | Qt::TextWordWrap, emptyString);
painter.restore(); painter.restore();
return; return;
} }

View File

@ -221,6 +221,9 @@ void LauncherPage::applySettings()
break; break;
} }
// Cat
s->set("CatOpacity", ui->catOpacitySpinBox->value());
// Mods // Mods
s->set("ModMetadataDisabled", ui->metadataDisableBtn->isChecked()); s->set("ModMetadataDisabled", ui->metadataDisableBtn->isChecked());
s->set("ModDependenciesDisabled", ui->dependenciesDisableBtn->isChecked()); s->set("ModDependenciesDisabled", ui->dependenciesDisableBtn->isChecked());
@ -276,6 +279,9 @@ void LauncherPage::loadSettings()
ui->sortByNameBtn->setChecked(true); ui->sortByNameBtn->setChecked(true);
} }
// Cat
ui->catOpacitySpinBox->setValue(s->get("CatOpacity").toInt());
// Mods // Mods
ui->metadataDisableBtn->setChecked(s->get("ModMetadataDisabled").toBool()); ui->metadataDisableBtn->setChecked(s->get("ModMetadataDisabled").toBool());
ui->metadataWarningLabel->setHidden(!ui->metadataDisableBtn->isChecked()); ui->metadataWarningLabel->setHidden(!ui->metadataDisableBtn->isChecked());

View File

@ -186,7 +186,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QCheckBox" name="dependenciesDisableBtn"> <widget class="QCheckBox" name="dependenciesDisableBtn">
<property name="toolTip"> <property name="toolTip">
<string>Disable the automatic detection, installation, and updating of mod dependencies.</string> <string>Disable the automatic detection, installation, and updating of mod dependencies.</string>
@ -300,6 +300,54 @@
</layout> </layout>
</widget> </widget>
</item> </item>
<item>
<widget class="QGroupBox" name="catBox">
<property name="title">
<string>Cat</string>
</property>
<layout class="QGridLayout" name="gridLayout_5">
<item row="0" column="0">
<widget class="QLabel" name="catOpacityLabel">
<property name="toolTip">
<string>Set the cat's opacity. 0% is fully transparent and 100% is fully opaque.</string>
</property>
<property name="text">
<string>Opacity</string>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QSpinBox" name="catOpacitySpinBox">
<property name="specialValueText">
<string/>
</property>
<property name="suffix">
<string>%</string>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="value">
<number>0</number>
</property>
</widget>
</item>
<item row="0" column="1">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
<item> <item>
<widget class="QGroupBox" name="toolsBox"> <widget class="QGroupBox" name="toolsBox">
<property name="sizePolicy"> <property name="sizePolicy">

View File

@ -109,6 +109,7 @@ void MinecraftPage::applySettings()
s->set("EnableFeralGamemode", ui->enableFeralGamemodeCheck->isChecked()); s->set("EnableFeralGamemode", ui->enableFeralGamemodeCheck->isChecked());
s->set("EnableMangoHud", ui->enableMangoHud->isChecked()); s->set("EnableMangoHud", ui->enableMangoHud->isChecked());
s->set("UseDiscreteGpu", ui->useDiscreteGpuCheck->isChecked()); s->set("UseDiscreteGpu", ui->useDiscreteGpuCheck->isChecked());
s->set("UseZink", ui->useZink->isChecked());
// Game time // Game time
s->set("ShowGameTime", ui->showGameTime->isChecked()); s->set("ShowGameTime", ui->showGameTime->isChecked());
@ -151,6 +152,7 @@ void MinecraftPage::loadSettings()
ui->enableFeralGamemodeCheck->setChecked(s->get("EnableFeralGamemode").toBool()); ui->enableFeralGamemodeCheck->setChecked(s->get("EnableFeralGamemode").toBool());
ui->enableMangoHud->setChecked(s->get("EnableMangoHud").toBool()); ui->enableMangoHud->setChecked(s->get("EnableMangoHud").toBool());
ui->useDiscreteGpuCheck->setChecked(s->get("UseDiscreteGpu").toBool()); ui->useDiscreteGpuCheck->setChecked(s->get("UseDiscreteGpu").toBool());
ui->useZink->setChecked(s->get("UseZink").toBool());
#if !defined(Q_OS_LINUX) #if !defined(Q_OS_LINUX)
ui->perfomanceGroupBox->setVisible(false); ui->perfomanceGroupBox->setVisible(false);

View File

@ -206,7 +206,7 @@
<item> <item>
<widget class="QCheckBox" name="onlineFixes"> <widget class="QCheckBox" name="onlineFixes">
<property name="toolTip"> <property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Emulates usages of old online services which are no longer operating.&lt;/p&gt;&lt;p&gt;This currently allows modern skins to be used.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string> <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Emulates usages of old online services which are no longer operating.&lt;/p&gt;&lt;p&gt;Current fixes include: skin and online mode support.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property> </property>
<property name="text"> <property name="text">
<string>Enable online fixes (experimental)</string> <string>Enable online fixes (experimental)</string>
@ -309,6 +309,16 @@
</property> </property>
</widget> </widget>
</item> </item>
<item>
<widget class="QCheckBox" name="useZink">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Use Zink, a Mesa OpenGL driver that implements OpenGL on top of Vulkan. Performance may vary depending on the situation. Note: If no suitable Vulkan driver is found, software rendering will be used.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Use Zink</string>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
</item> </item>

View File

@ -290,12 +290,12 @@ void ExternalResourcesPage::disableItem()
void ExternalResourcesPage::viewConfigs() void ExternalResourcesPage::viewConfigs()
{ {
DesktopServices::openDirectory(m_instance->instanceConfigFolder(), true); DesktopServices::openPath(m_instance->instanceConfigFolder(), true);
} }
void ExternalResourcesPage::viewFolder() void ExternalResourcesPage::viewFolder()
{ {
DesktopServices::openDirectory(m_model->dir().absolutePath(), true); DesktopServices::openPath(m_model->dir().absolutePath(), true);
} }
bool ExternalResourcesPage::current(const QModelIndex& current, const QModelIndex& previous) bool ExternalResourcesPage::current(const QModelIndex& current, const QModelIndex& previous)

View File

@ -232,10 +232,13 @@ void InstanceSettingsPage::applySettings()
m_settings->set("EnableFeralGamemode", ui->enableFeralGamemodeCheck->isChecked()); m_settings->set("EnableFeralGamemode", ui->enableFeralGamemodeCheck->isChecked());
m_settings->set("EnableMangoHud", ui->enableMangoHud->isChecked()); m_settings->set("EnableMangoHud", ui->enableMangoHud->isChecked());
m_settings->set("UseDiscreteGpu", ui->useDiscreteGpuCheck->isChecked()); m_settings->set("UseDiscreteGpu", ui->useDiscreteGpuCheck->isChecked());
m_settings->set("UseZink", ui->useZink->isChecked());
} else { } else {
m_settings->reset("EnableFeralGamemode"); m_settings->reset("EnableFeralGamemode");
m_settings->reset("EnableMangoHud"); m_settings->reset("EnableMangoHud");
m_settings->reset("UseDiscreteGpu"); m_settings->reset("UseDiscreteGpu");
m_settings->reset("UseZink");
} }
// Game time // Game time
@ -346,7 +349,7 @@ void InstanceSettingsPage::loadSettings()
#ifdef Q_OS_LINUX #ifdef Q_OS_LINUX
ui->lineEditOpenALPath->setPlaceholderText(APPLICATION->m_detectedOpenALPath); ui->lineEditOpenALPath->setPlaceholderText(APPLICATION->m_detectedOpenALPath);
#else #else
ui->lineEditGLFWPath->setPlaceholderText(tr("Path to %1 library file").arg(BuildConfig.OPENAL_LIBRARY_NAME)); ui->lineEditOpenALPath->setPlaceholderText(tr("Path to %1 library file").arg(BuildConfig.OPENAL_LIBRARY_NAME));
#endif #endif
// Performance // Performance
@ -354,6 +357,7 @@ void InstanceSettingsPage::loadSettings()
ui->enableFeralGamemodeCheck->setChecked(m_settings->get("EnableFeralGamemode").toBool()); ui->enableFeralGamemodeCheck->setChecked(m_settings->get("EnableFeralGamemode").toBool());
ui->enableMangoHud->setChecked(m_settings->get("EnableMangoHud").toBool()); ui->enableMangoHud->setChecked(m_settings->get("EnableMangoHud").toBool());
ui->useDiscreteGpuCheck->setChecked(m_settings->get("UseDiscreteGpu").toBool()); ui->useDiscreteGpuCheck->setChecked(m_settings->get("UseDiscreteGpu").toBool());
ui->useZink->setChecked(m_settings->get("UseZink").toBool());
#if !defined(Q_OS_LINUX) #if !defined(Q_OS_LINUX)
ui->settingsTabs->setTabVisible(ui->settingsTabs->indexOf(ui->performancePage), false); ui->settingsTabs->setTabVisible(ui->settingsTabs->indexOf(ui->performancePage), false);

View File

@ -567,6 +567,16 @@
</property> </property>
</widget> </widget>
</item> </item>
<item>
<widget class="QCheckBox" name="useZink">
<property name="toolTip">
<string>Use Zink, a Mesa OpenGL driver that implements OpenGL on top of Vulkan. Performance may vary depending on the situation. Note: If no suitable Vulkan driver is found, software rendering will be used.</string>
</property>
<property name="text">
<string>Use Zink</string>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
</item> </item>
@ -605,7 +615,7 @@
<item> <item>
<widget class="QCheckBox" name="onlineFixes"> <widget class="QCheckBox" name="onlineFixes">
<property name="toolTip"> <property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Emulates usages of old online services which are no longer operating.&lt;/p&gt;&lt;p&gt;This currently allows modern skins to be used.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string> <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Emulates usages of old online services which are no longer operating.&lt;/p&gt;&lt;p&gt;Current fixes include: skin and online mode support.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property> </property>
<property name="text"> <property name="text">
<string>Enable online fixes (experimental)</string> <string>Enable online fixes (experimental)</string>

View File

@ -247,7 +247,7 @@ void ModFolderPage::updateMods(bool includeDeps)
if (m_instance != nullptr && m_instance->isRunning()) { if (m_instance != nullptr && m_instance->isRunning()) {
auto response = auto response =
CustomMessageBox::selectable(this, tr("Confirm Update"), CustomMessageBox::selectable(this, tr("Confirm Update"),
tr("If you update mods while the game is running may cause mod duplication and game crashes.\n" tr("Updating mods while the game is running may cause mod duplication and game crashes.\n"
"The old files may not be deleted as they are in use.\n" "The old files may not be deleted as they are in use.\n"
"Are you sure you want to do this?"), "Are you sure you want to do this?"),
QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No)

View File

@ -324,8 +324,7 @@ void ScreenshotsPage::onItemActivated(QModelIndex index)
if (!index.isValid()) if (!index.isValid())
return; return;
auto info = m_model->fileInfo(index); auto info = m_model->fileInfo(index);
QString fileName = info.absoluteFilePath(); DesktopServices::openPath(info);
DesktopServices::openFile(info.absoluteFilePath());
} }
void ScreenshotsPage::onCurrentSelectionChanged(const QItemSelection& selected) void ScreenshotsPage::onCurrentSelectionChanged(const QItemSelection& selected)
@ -352,7 +351,7 @@ void ScreenshotsPage::onCurrentSelectionChanged(const QItemSelection& selected)
void ScreenshotsPage::on_actionView_Folder_triggered() void ScreenshotsPage::on_actionView_Folder_triggered()
{ {
DesktopServices::openDirectory(m_folder, true); DesktopServices::openPath(m_folder, true);
} }
void ScreenshotsPage::on_actionUpload_triggered() void ScreenshotsPage::on_actionUpload_triggered()

View File

@ -447,12 +447,12 @@ void VersionPage::on_actionAdd_Empty_triggered()
void VersionPage::on_actionLibrariesFolder_triggered() void VersionPage::on_actionLibrariesFolder_triggered()
{ {
DesktopServices::openDirectory(m_inst->getLocalLibraryPath(), true); DesktopServices::openPath(m_inst->getLocalLibraryPath(), true);
} }
void VersionPage::on_actionMinecraftFolder_triggered() void VersionPage::on_actionMinecraftFolder_triggered()
{ {
DesktopServices::openDirectory(m_inst->gameRoot(), true); DesktopServices::openPath(m_inst->gameRoot(), true);
} }
void VersionPage::versionCurrent(const QModelIndex& current, [[maybe_unused]] const QModelIndex& previous) void VersionPage::versionCurrent(const QModelIndex& current, [[maybe_unused]] const QModelIndex& previous)

View File

@ -207,7 +207,7 @@ void WorldListPage::on_actionRemove_triggered()
void WorldListPage::on_actionView_Folder_triggered() void WorldListPage::on_actionView_Folder_triggered()
{ {
DesktopServices::openDirectory(m_worlds->dir().absolutePath(), true); DesktopServices::openPath(m_worlds->dir().absolutePath(), true);
} }
void WorldListPage::on_actionDatapacks_triggered() void WorldListPage::on_actionDatapacks_triggered()
@ -223,7 +223,7 @@ void WorldListPage::on_actionDatapacks_triggered()
auto fullPath = m_worlds->data(index, WorldList::FolderRole).toString(); auto fullPath = m_worlds->data(index, WorldList::FolderRole).toString();
DesktopServices::openDirectory(FS::PathCombine(fullPath, "datapacks"), true); DesktopServices::openPath(FS::PathCombine(fullPath, "datapacks"), true);
} }
void WorldListPage::on_actionReset_Icon_triggered() void WorldListPage::on_actionReset_Icon_triggered()

View File

@ -123,6 +123,10 @@ void ImportPage::updateState()
// need to find the download link for the modpack // need to find the download link for the modpack
// format of url curseforge://install?addonId=IDHERE&fileId=IDHERE // format of url curseforge://install?addonId=IDHERE&fileId=IDHERE
QUrlQuery query(url); QUrlQuery query(url);
if (query.allQueryItemValues("addonId").isEmpty() || query.allQueryItemValues("fileId").isEmpty()) {
qDebug() << "Invalid curseforge link:" << url;
return;
}
auto addonId = query.allQueryItemValues("addonId")[0]; auto addonId = query.allQueryItemValues("addonId")[0];
auto fileId = query.allQueryItemValues("fileId")[0]; auto fileId = query.allQueryItemValues("fileId")[0];
auto array = std::make_shared<QByteArray>(); auto array = std::make_shared<QByteArray>();
@ -200,7 +204,9 @@ void ImportPage::setExtraInfo(const QMap<QString, QString>& extra_info)
void ImportPage::on_modpackBtn_clicked() void ImportPage::on_modpackBtn_clicked()
{ {
auto filter = QMimeDatabase().mimeTypeForName("application/zip").filterString(); const QMimeType zip = QMimeDatabase().mimeTypeForName("application/zip");
auto filter = tr("Supported files") + QString(" (%1 *.mrpack)").arg(zip.globPatterns().join(" "));
filter += ";;" + zip.filterString();
//: Option for filtering for *.mrpack files when importing //: Option for filtering for *.mrpack files when importing
filter += ";;" + tr("Modrinth pack") + " (*.mrpack)"; filter += ";;" + tr("Modrinth pack") + " (*.mrpack)";
const QUrl url = QFileDialog::getOpenFileUrl(this, tr("Choose modpack"), modpackUrl(), filter); const QUrl url = QFileDialog::getOpenFileUrl(this, tr("Choose modpack"), modpackUrl(), filter);

View File

@ -90,6 +90,7 @@ void ModPage::filterMods()
void ModPage::triggerSearch() void ModPage::triggerSearch()
{ {
m_filter = m_filter_widget->getFilter(); m_filter = m_filter_widget->getFilter();
m_ui->packView->selectionModel()->setCurrentIndex({}, QItemSelectionModel::SelectionFlag::ClearAndSelect);
m_ui->packView->clearSelection(); m_ui->packView->clearSelection();
m_ui->packDescription->clear(); m_ui->packDescription->clear();
m_ui->versionSelectionBox->clear(); m_ui->versionSelectionBox->clear();

View File

@ -209,7 +209,8 @@ void ResourceModel::loadEntry(QModelIndex& entry)
}; };
if (!callbacks.on_fail) if (!callbacks.on_fail)
callbacks.on_fail = [](QString reason, int) { callbacks.on_fail = [](QString reason, int) {
QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load project versions:%1").arg(reason)); QMessageBox::critical(nullptr, tr("Error"),
tr("A network error occurred. Could not load project versions: %1").arg(reason));
}; };
if (auto job = m_api->getProjectVersions(std::move(args), std::move(callbacks)); job) if (auto job = m_api->getProjectVersions(std::move(args), std::move(callbacks)); job)
@ -232,13 +233,13 @@ void ResourceModel::loadEntry(QModelIndex& entry)
callbacks.on_fail = [this](QString reason) { callbacks.on_fail = [this](QString reason) {
if (!s_running_models.constFind(this).value()) if (!s_running_models.constFind(this).value())
return; return;
QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load project info:%1").arg(reason)); QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load project info: %1").arg(reason));
}; };
if (!callbacks.on_abort) if (!callbacks.on_abort)
callbacks.on_abort = [this] { callbacks.on_abort = [this] {
if (!s_running_models.constFind(this).value()) if (!s_running_models.constFind(this).value())
return; return;
qCritical() << tr("The request was abborted for an unknown reason"); qCritical() << tr("The request was aborted for an unknown reason");
}; };
if (auto job = m_api->getProjectInfo(std::move(args), std::move(callbacks)); job) if (auto job = m_api->getProjectInfo(std::move(args), std::move(callbacks)); job)

View File

@ -23,6 +23,7 @@ ResourcePackResourcePage::ResourcePackResourcePage(ResourceDownloadDialog* dialo
void ResourcePackResourcePage::triggerSearch() void ResourcePackResourcePage::triggerSearch()
{ {
m_ui->packView->selectionModel()->setCurrentIndex({}, QItemSelectionModel::SelectionFlag::ClearAndSelect);
m_ui->packView->clearSelection(); m_ui->packView->clearSelection();
m_ui->packDescription->clear(); m_ui->packDescription->clear();
m_ui->versionSelectionBox->clear(); m_ui->versionSelectionBox->clear();

View File

@ -200,6 +200,11 @@ void ResourcePage::updateUi()
} }
if (current_pack->extraDataLoaded) { if (current_pack->extraDataLoaded) {
if (current_pack->extraData.status == "archived") {
text += "<br><br>" + tr("<b>This project has been archived. It will not receive any further updates unless the author decides "
"to unarchive the project.</b>");
}
if (!current_pack->extraData.donate.isEmpty()) { if (!current_pack->extraData.donate.isEmpty()) {
text += "<br><br>" + tr("Donate information: "); text += "<br><br>" + tr("Donate information: ");
auto donateToStr = [](ModPlatform::DonationData& donate) -> QString { auto donateToStr = [](ModPlatform::DonationData& donate) -> QString {
@ -404,9 +409,9 @@ void ResourcePage::openUrl(const QUrl& url)
auto jump = [url, slug, model, view] { auto jump = [url, slug, model, view] {
for (int row = 0; row < model->rowCount({}); row++) { for (int row = 0; row < model->rowCount({}); row++) {
const QModelIndex index = model->index(row); const QModelIndex index = model->index(row);
const auto pack = model->data(index, Qt::UserRole).value<ModPlatform::IndexedPack>(); const auto pack = model->data(index, Qt::UserRole).value<ModPlatform::IndexedPack::Ptr>();
if (pack.slug == slug) { if (pack->slug == slug) {
view->setCurrentIndex(index); view->setCurrentIndex(index);
return; return;
} }

View File

@ -24,6 +24,7 @@ ShaderPackResourcePage::ShaderPackResourcePage(ShaderPackDownloadDialog* dialog,
void ShaderPackResourcePage::triggerSearch() void ShaderPackResourcePage::triggerSearch()
{ {
m_ui->packView->selectionModel()->setCurrentIndex({}, QItemSelectionModel::SelectionFlag::ClearAndSelect);
m_ui->packView->clearSelection(); m_ui->packView->clearSelection();
m_ui->packDescription->clear(); m_ui->packDescription->clear();
m_ui->versionSelectionBox->clear(); m_ui->versionSelectionBox->clear();

View File

@ -20,6 +20,7 @@
#include "ui/widgets/ProjectItem.h" #include "ui/widgets/ProjectItem.h"
#include "ui_ImportFTBPage.h" #include "ui_ImportFTBPage.h"
#include <QFileDialog>
#include <QWidget> #include <QWidget>
#include "FileSystem.h" #include "FileSystem.h"
#include "ListModel.h" #include "ListModel.h"
@ -56,6 +57,13 @@ ImportFTBPage::ImportFTBPage(NewInstanceDialog* dialog, QWidget* parent) : QWidg
connect(ui->searchEdit, &QLineEdit::textChanged, this, &ImportFTBPage::triggerSearch); connect(ui->searchEdit, &QLineEdit::textChanged, this, &ImportFTBPage::triggerSearch);
connect(ui->browseButton, &QPushButton::clicked, this, [this] {
auto path = listModel->getPath();
QString dir = QFileDialog::getExistingDirectory(this, tr("Select FTBApp instances directory"), path, QFileDialog::ShowDirsOnly);
if (!dir.isEmpty())
listModel->setPath(dir);
});
ui->modpackList->setItemDelegate(new ProjectItemDelegate(this)); ui->modpackList->setItemDelegate(new ProjectItemDelegate(this));
ui->modpackList->selectionModel()->reset(); ui->modpackList->selectionModel()->reset();
} }

View File

@ -11,7 +11,7 @@
</rect> </rect>
</property> </property>
<layout class="QGridLayout" name="gridLayout"> <layout class="QGridLayout" name="gridLayout">
<item row="1" column="1"> <item row="2" column="1">
<widget class="QTreeView" name="modpackList"> <widget class="QTreeView" name="modpackList">
<property name="maximumSize"> <property name="maximumSize">
<size> <size>
@ -21,28 +21,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="0" column="1"> <item row="3" column="1">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLineEdit" name="searchEdit">
<property name="placeholderText">
<string>Search and filter...</string>
</property>
<property name="clearButtonEnabled">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton">
<property name="text">
<string>Search</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_2"> <layout class="QHBoxLayout" name="horizontalLayout_2">
<item> <item>
<widget class="QComboBox" name="sortByBox"> <widget class="QComboBox" name="sortByBox">
@ -69,6 +48,54 @@
</item> </item>
</layout> </layout>
</item> </item>
<item row="1" column="1">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLineEdit" name="searchEdit">
<property name="placeholderText">
<string>Search and filter...</string>
</property>
<property name="clearButtonEnabled">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton">
<property name="text">
<string>Search</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="browseButton">
<property name="toolTip">
<string>Select FTBApp instances directory</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset theme="viewfolder">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="flat">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item row="0" column="1">
<widget class="QLabel" name="label">
<property name="text">
<string>Note: If your FTB instances are not in the default location, select it using the button next to search.</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
<resources/> <resources/>

View File

@ -17,11 +17,13 @@
*/ */
#include "ListModel.h" #include "ListModel.h"
#include <qfileinfo.h>
#include <QDir> #include <QDir>
#include <QDirIterator> #include <QDirIterator>
#include <QFileInfo> #include <QFileInfo>
#include <QIcon> #include <QIcon>
#include <QProcessEnvironment> #include <QProcessEnvironment>
#include "Application.h"
#include "FileSystem.h" #include "FileSystem.h"
#include "StringUtils.h" #include "StringUtils.h"
#include "modplatform/import_ftb/PackHelpers.h" #include "modplatform/import_ftb/PackHelpers.h"
@ -29,7 +31,7 @@
namespace FTBImportAPP { namespace FTBImportAPP {
QString getPath() QString getStaticPath()
{ {
QString partialPath; QString partialPath;
#if defined(Q_OS_OSX) #if defined(Q_OS_OSX)
@ -42,14 +44,14 @@ QString getPath()
return FS::PathCombine(partialPath, ".ftba"); return FS::PathCombine(partialPath, ".ftba");
} }
const QString ListModel::FTB_APP_PATH = getPath(); static const QString FTB_APP_PATH = FS::PathCombine(getStaticPath(), "instances");
void ListModel::update() void ListModel::update()
{ {
beginResetModel(); beginResetModel();
modpacks.clear(); modpacks.clear();
QString instancesPath = FS::PathCombine(FTB_APP_PATH, "instances"); QString instancesPath = getPath();
if (auto instancesInfo = QFileInfo(instancesPath); instancesInfo.exists() && instancesInfo.isDir()) { if (auto instancesInfo = QFileInfo(instancesPath); instancesInfo.exists() && instancesInfo.isDir()) {
QDirIterator directoryIterator(instancesPath, QDir::Dirs | QDir::NoDotAndDotDot | QDir::Readable | QDir::Hidden, QDirIterator directoryIterator(instancesPath, QDir::Dirs | QDir::NoDotAndDotDot | QDir::Readable | QDir::Hidden,
QDirIterator::FollowSymlinks); QDirIterator::FollowSymlinks);
@ -168,4 +170,17 @@ FilterModel::Sorting FilterModel::getCurrentSorting()
{ {
return currentSorting; return currentSorting;
} }
void ListModel::setPath(QString path)
{
APPLICATION->settings()->set("FTBAppInstancesPath", path);
update();
}
QString ListModel::getPath()
{
auto path = APPLICATION->settings()->get("FTBAppInstancesPath").toString();
if (path.isEmpty() || !QFileInfo(path).exists())
path = FTB_APP_PATH;
return path;
}
} // namespace FTBImportAPP } // namespace FTBImportAPP

View File

@ -60,7 +60,8 @@ class ListModel : public QAbstractListModel {
void update(); void update();
static const QString FTB_APP_PATH; QString getPath();
void setPath(QString path);
private: private:
ModpackList modpacks; ModpackList modpacks;

View File

@ -142,7 +142,7 @@ void ModpackListModel::performPaginatedSearch()
callbacks.on_succeed = [this](auto& doc, auto& pack) { searchRequestForOneSucceeded(doc); }; callbacks.on_succeed = [this](auto& doc, auto& pack) { searchRequestForOneSucceeded(doc); };
callbacks.on_abort = [this] { callbacks.on_abort = [this] {
qCritical() << "Search task aborted by an unknown reason!"; qCritical() << "Search task aborted by an unknown reason!";
searchRequestFailed("Abborted"); searchRequestFailed("Aborted");
}; };
static const ModrinthAPI api; static const ModrinthAPI api;
if (auto job = api.getProjectInfo({ projectId }, std::move(callbacks)); job) { if (auto job = api.getProjectInfo({ projectId }, std::move(callbacks)); job) {

View File

@ -104,6 +104,7 @@ void ModrinthPage::retranslate()
void ModrinthPage::openedImpl() void ModrinthPage::openedImpl()
{ {
BasePage::openedImpl(); BasePage::openedImpl();
suggestCurrent();
triggerSearch(); triggerSearch();
} }
@ -267,6 +268,11 @@ void ModrinthPage::updateUI()
text += "<br>" + tr(" by ") + QString("<a href=%1>%2</a>").arg(std::get<1>(current.author).toString(), std::get<0>(current.author)); text += "<br>" + tr(" by ") + QString("<a href=%1>%2</a>").arg(std::get<1>(current.author).toString(), std::get<0>(current.author));
if (current.extraInfoLoaded) { if (current.extraInfoLoaded) {
if (current.extra.status == "archived") {
text += "<br><br>" + tr("<b>This project has been archived. It will not receive any further updates unless the author decides "
"to unarchive the project.</b>");
}
if (!current.extra.donate.isEmpty()) { if (!current.extra.donate.isEmpty()) {
text += "<br><br>" + tr("Donate information: "); text += "<br><br>" + tr("Donate information: ");
auto donateToStr = [](Modrinth::DonationData& donate) -> QString { auto donateToStr = [](Modrinth::DonationData& donate) -> QString {

View File

@ -11,43 +11,7 @@
</rect> </rect>
</property> </property>
<layout class="QGridLayout" name="gridLayout"> <layout class="QGridLayout" name="gridLayout">
<item row="0" column="0"> <item row="2" column="0">
<widget class="QLabel" name="label_2">
<property name="font">
<font>
<italic>true</italic>
</font>
</property>
<property name="text">
<string>Note: Modrinth modpacks are still in alpha phase. Some things may be rough on the edges, or not working at all! Use it with caution.</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<layout class="QHBoxLayout">
<item>
<widget class="QLineEdit" name="searchEdit">
<property name="placeholderText">
<string>Search and filter ...</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="searchButton">
<property name="text">
<string>Search</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="3" column="0">
<layout class="QHBoxLayout"> <layout class="QHBoxLayout">
<item> <item>
<widget class="QListView" name="packView"> <widget class="QListView" name="packView">
@ -77,7 +41,7 @@
</item> </item>
</layout> </layout>
</item> </item>
<item row="4" column="0"> <item row="3" column="0">
<layout class="QHBoxLayout"> <layout class="QHBoxLayout">
<item> <item>
<widget class="QComboBox" name="sortByBox"/> <widget class="QComboBox" name="sortByBox"/>
@ -97,6 +61,24 @@
</item> </item>
</layout> </layout>
</item> </item>
<item row="0" column="0">
<layout class="QHBoxLayout">
<item>
<widget class="QLineEdit" name="searchEdit">
<property name="placeholderText">
<string>Search and filter ...</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="searchButton">
<property name="text">
<string>Search</string>
</property>
</widget>
</item>
</layout>
</item>
</layout> </layout>
</widget> </widget>
<customwidgets> <customwidgets>

View File

@ -36,7 +36,10 @@
#include "ui/themes/CatPack.h" #include "ui/themes/CatPack.h"
#include <QDate> #include <QDate>
#include <QDir> #include <QDir>
#include <QDirIterator>
#include <QFileInfo> #include <QFileInfo>
#include <QImageReader>
#include <QRandomGenerator>
#include "FileSystem.h" #include "FileSystem.h"
#include "Json.h" #include "Json.h"
@ -79,7 +82,7 @@ JsonCatPack::JsonCatPack(QFileInfo& manifestInfo) : BasicCatPack(manifestInfo.di
auto doc = Json::requireDocument(manifestInfo.absoluteFilePath(), "CatPack JSON file"); auto doc = Json::requireDocument(manifestInfo.absoluteFilePath(), "CatPack JSON file");
const auto root = doc.object(); const auto root = doc.object();
m_name = Json::requireString(root, "name", "Catpack name"); m_name = Json::requireString(root, "name", "Catpack name");
m_defaultPath = FS::PathCombine(path, Json::requireString(root, "default", "Default Cat")); m_default_path = FS::PathCombine(path, Json::requireString(root, "default", "Default Cat"));
auto variants = Json::ensureArray(root, "variants", QJsonArray(), "Catpack Variants"); auto variants = Json::ensureArray(root, "variants", QJsonArray(), "Catpack Variants");
for (auto v : variants) { for (auto v : variants) {
auto variant = Json::ensureObject(v, QJsonObject(), "Cat variant"); auto variant = Json::ensureObject(v, QJsonObject(), "Cat variant");
@ -117,5 +120,21 @@ QString JsonCatPack::path(QDate now)
if (startDate <= now && now <= endDate) if (startDate <= now && now <= endDate)
return var.path; return var.path;
} }
return m_defaultPath; auto dInfo = QFileInfo(m_default_path);
if (!dInfo.isDir())
return m_default_path;
QStringList supportedImageFormats;
for (auto format : QImageReader::supportedImageFormats()) {
supportedImageFormats.append("*." + format);
}
auto files = QDir(m_default_path).entryInfoList(supportedImageFormats, QDir::Files, QDir::Name);
if (files.length() == 0)
return "";
auto idx = (now.dayOfYear() - 1) % files.length();
auto isRandom = dInfo.fileName().compare("random", Qt::CaseInsensitive) == 0;
if (isRandom)
idx = QRandomGenerator::global()->bounded(0, files.length());
return files[idx].absoluteFilePath();
} }

View File

@ -87,6 +87,6 @@ class JsonCatPack : public BasicCatPack {
QString path(QDate now); QString path(QDate now);
private: private:
QString m_defaultPath; QString m_default_path;
QList<Variant> m_variants; QList<Variant> m_variants;
}; };

View File

@ -178,8 +178,8 @@ QList<ITheme*> ThemeManager::getValidApplicationThemes()
QList<CatPack*> ThemeManager::getValidCatPacks() QList<CatPack*> ThemeManager::getValidCatPacks()
{ {
QList<CatPack*> ret; QList<CatPack*> ret;
ret.reserve(m_catPacks.size()); ret.reserve(m_cat_packs.size());
for (auto&& [id, theme] : m_catPacks) { for (auto&& [id, theme] : m_cat_packs) {
ret.append(theme.get()); ret.append(theme.get());
} }
return ret; return ret;
@ -244,8 +244,8 @@ void ThemeManager::applyCurrentlySelectedTheme(bool initial)
QString ThemeManager::getCatPack(QString catName) QString ThemeManager::getCatPack(QString catName)
{ {
auto catIter = m_catPacks.find(!catName.isEmpty() ? catName : APPLICATION->settings()->get("BackgroundCat").toString()); auto catIter = m_cat_packs.find(!catName.isEmpty() ? catName : APPLICATION->settings()->get("BackgroundCat").toString());
if (catIter != m_catPacks.end()) { if (catIter != m_cat_packs.end()) {
auto& catPack = catIter->second; auto& catPack = catIter->second;
themeDebugLog() << "applying catpack" << catPack->id(); themeDebugLog() << "applying catpack" << catPack->id();
return catPack->path(); return catPack->path();
@ -253,14 +253,14 @@ QString ThemeManager::getCatPack(QString catName)
themeWarningLog() << "Tried to get invalid catPack:" << catName; themeWarningLog() << "Tried to get invalid catPack:" << catName;
} }
return m_catPacks.begin()->second->path(); return m_cat_packs.begin()->second->path();
} }
QString ThemeManager::addCatPack(std::unique_ptr<CatPack> catPack) QString ThemeManager::addCatPack(std::unique_ptr<CatPack> catPack)
{ {
QString id = catPack->id(); QString id = catPack->id();
if (m_catPacks.find(id) == m_catPacks.end()) if (m_cat_packs.find(id) == m_cat_packs.end())
m_catPacks.emplace(id, std::move(catPack)); m_cat_packs.emplace(id, std::move(catPack));
else else
themeWarningLog() << "CatPack(" << id << ") not added to prevent id duplication"; themeWarningLog() << "CatPack(" << id << ") not added to prevent id duplication";
return id; return id;

View File

@ -61,7 +61,7 @@ class ThemeManager {
QDir m_iconThemeFolder{ "iconthemes" }; QDir m_iconThemeFolder{ "iconthemes" };
QDir m_applicationThemeFolder{ "themes" }; QDir m_applicationThemeFolder{ "themes" };
QDir m_catPacksFolder{ "catpacks" }; QDir m_catPacksFolder{ "catpacks" };
std::map<QString, std::unique_ptr<CatPack>> m_catPacks; std::map<QString, std::unique_ptr<CatPack>> m_cat_packs;
void initializeThemes(); void initializeThemes();
void initializeCatPacks(); void initializeCatPacks();

View File

@ -36,6 +36,7 @@
#include "LogView.h" #include "LogView.h"
#include <QScrollBar> #include <QScrollBar>
#include <QTextBlock> #include <QTextBlock>
#include <QTextDocumentFragment>
LogView::LogView(QWidget* parent) : QPlainTextEdit(parent) LogView::LogView(QWidget* parent) : QPlainTextEdit(parent)
{ {
@ -117,6 +118,9 @@ void LogView::rowsAboutToBeInserted(const QModelIndex& parent, int first, int la
void LogView::rowsInserted(const QModelIndex& parent, int first, int last) void LogView::rowsInserted(const QModelIndex& parent, int first, int last)
{ {
QTextDocument document;
QTextCursor cursor(&document);
for (int i = first; i <= last; i++) { for (int i = first; i <= last; i++) {
auto idx = m_model->index(i, 0, parent); auto idx = m_model->index(i, 0, parent);
auto text = m_model->data(idx, Qt::DisplayRole).toString(); auto text = m_model->data(idx, Qt::DisplayRole).toString();
@ -133,11 +137,16 @@ void LogView::rowsInserted(const QModelIndex& parent, int first, int last)
if (bg.isValid()) { if (bg.isValid()) {
format.setBackground(bg.value<QColor>()); format.setBackground(bg.value<QColor>());
} }
auto workCursor = textCursor(); cursor.movePosition(QTextCursor::End);
workCursor.movePosition(QTextCursor::End); cursor.insertText(text, format);
workCursor.insertText(text, format); cursor.insertBlock();
workCursor.insertBlock();
} }
QTextDocumentFragment fragment(&document);
QTextCursor workCursor = textCursor();
workCursor.movePosition(QTextCursor::End);
workCursor.insertFragment(fragment);
if (m_scroll && !m_scrolling) { if (m_scroll && !m_scrolling) {
m_scrolling = true; m_scrolling = true;
QMetaObject::invokeMethod(this, "scrollToBottom", Qt::QueuedConnection); QMetaObject::invokeMethod(this, "scrollToBottom", Qt::QueuedConnection);

View File

@ -88,7 +88,7 @@ void ProjectItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& o
} }
{ // Description painting { // Description painting
auto description = index.data(UserDataTypes::DESCRIPTION).toString(); auto description = index.data(UserDataTypes::DESCRIPTION).toString().simplified();
QTextLayout text_layout(description, opt.font); QTextLayout text_layout(description, opt.font);

View File

@ -34,11 +34,11 @@ ThemeCustomizationWidget::ThemeCustomizationWidget(QWidget* parent) : QWidget(pa
connect(ui->backgroundCatComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &ThemeCustomizationWidget::applyCatTheme); connect(ui->backgroundCatComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &ThemeCustomizationWidget::applyCatTheme);
connect(ui->iconsFolder, &QPushButton::clicked, this, connect(ui->iconsFolder, &QPushButton::clicked, this,
[] { DesktopServices::openDirectory(APPLICATION->themeManager()->getIconThemesFolder().path()); }); [] { DesktopServices::openPath(APPLICATION->themeManager()->getIconThemesFolder().path()); });
connect(ui->widgetStyleFolder, &QPushButton::clicked, this, connect(ui->widgetStyleFolder, &QPushButton::clicked, this,
[] { DesktopServices::openDirectory(APPLICATION->themeManager()->getApplicationThemesFolder().path()); }); [] { DesktopServices::openPath(APPLICATION->themeManager()->getApplicationThemesFolder().path()); });
connect(ui->catPackFolder, &QPushButton::clicked, this, connect(ui->catPackFolder, &QPushButton::clicked, this,
[] { DesktopServices::openDirectory(APPLICATION->themeManager()->getCatPacksFolder().path()); }); [] { DesktopServices::openPath(APPLICATION->themeManager()->getCatPacksFolder().path()); });
} }
ThemeCustomizationWidget::~ThemeCustomizationWidget() ThemeCustomizationWidget::~ThemeCustomizationWidget()

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