mirror of
https://github.com/PrismLauncher/PrismLauncher.git
synced 2025-04-29 22:24:26 +02:00
Merge remote-tracking branch 'upstream/develop' into unify-mc-settings
Signed-off-by: TheKodeToad <TheKodeToad@proton.me>
This commit is contained in:
commit
cc504f4a6c
75
.github/workflows/build.yml
vendored
75
.github/workflows/build.yml
vendored
@ -62,7 +62,7 @@ jobs:
|
|||||||
qt_version: "5.15.2"
|
qt_version: "5.15.2"
|
||||||
qt_modules: "qtnetworkauth"
|
qt_modules: "qtnetworkauth"
|
||||||
|
|
||||||
- os: ubuntu-20.04
|
- os: ubuntu-22.04
|
||||||
qt_ver: 6
|
qt_ver: 6
|
||||||
qt_host: linux
|
qt_host: linux
|
||||||
qt_arch: ""
|
qt_arch: ""
|
||||||
@ -80,9 +80,9 @@ jobs:
|
|||||||
architecture: "x64"
|
architecture: "x64"
|
||||||
vcvars_arch: "amd64"
|
vcvars_arch: "amd64"
|
||||||
qt_ver: 6
|
qt_ver: 6
|
||||||
qt_host: windows
|
qt_host: "windows"
|
||||||
qt_arch: ""
|
qt_arch: "win64_msvc2022_64"
|
||||||
qt_version: "6.7.3"
|
qt_version: "6.8.1"
|
||||||
qt_modules: "qt5compat qtimageformats qtnetworkauth"
|
qt_modules: "qt5compat qtimageformats qtnetworkauth"
|
||||||
nscurl_tag: "v24.9.26.122"
|
nscurl_tag: "v24.9.26.122"
|
||||||
nscurl_sha256: "AEE6C4BE3CB6455858E9C1EE4B3AFE0DB9960FA03FE99CCDEDC28390D57CCBB0"
|
nscurl_sha256: "AEE6C4BE3CB6455858E9C1EE4B3AFE0DB9960FA03FE99CCDEDC28390D57CCBB0"
|
||||||
@ -93,9 +93,9 @@ jobs:
|
|||||||
architecture: "arm64"
|
architecture: "arm64"
|
||||||
vcvars_arch: "amd64_arm64"
|
vcvars_arch: "amd64_arm64"
|
||||||
qt_ver: 6
|
qt_ver: 6
|
||||||
qt_host: windows
|
qt_host: "windows"
|
||||||
qt_arch: "win64_msvc2019_arm64"
|
qt_arch: "win64_msvc2022_arm64_cross_compiled"
|
||||||
qt_version: "6.7.3"
|
qt_version: "6.8.1"
|
||||||
qt_modules: "qt5compat qtimageformats qtnetworkauth"
|
qt_modules: "qt5compat qtimageformats qtnetworkauth"
|
||||||
nscurl_tag: "v24.9.26.122"
|
nscurl_tag: "v24.9.26.122"
|
||||||
nscurl_sha256: "AEE6C4BE3CB6455858E9C1EE4B3AFE0DB9960FA03FE99CCDEDC28390D57CCBB0"
|
nscurl_sha256: "AEE6C4BE3CB6455858E9C1EE4B3AFE0DB9960FA03FE99CCDEDC28390D57CCBB0"
|
||||||
@ -106,7 +106,7 @@ jobs:
|
|||||||
qt_ver: 6
|
qt_ver: 6
|
||||||
qt_host: mac
|
qt_host: mac
|
||||||
qt_arch: ""
|
qt_arch: ""
|
||||||
qt_version: "6.7.3"
|
qt_version: "6.8.1"
|
||||||
qt_modules: "qt5compat qtimageformats qtnetworkauth"
|
qt_modules: "qt5compat qtimageformats qtnetworkauth"
|
||||||
|
|
||||||
- os: macos-14
|
- os: macos-14
|
||||||
@ -167,13 +167,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.14
|
uses: hendrikmuhs/ccache-action@v1.2.16
|
||||||
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@v4.1.2
|
uses: actions/cache@v4.2.0
|
||||||
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 }}
|
||||||
@ -216,14 +216,14 @@ jobs:
|
|||||||
|
|
||||||
- name: Install host Qt (Windows MSVC arm64)
|
- name: Install host Qt (Windows MSVC arm64)
|
||||||
if: runner.os == 'Windows' && matrix.architecture == 'arm64'
|
if: runner.os == 'Windows' && matrix.architecture == 'arm64'
|
||||||
uses: jurplel/install-qt-action@v3
|
uses: jurplel/install-qt-action@v4
|
||||||
with:
|
with:
|
||||||
aqtversion: "==3.1.*"
|
aqtversion: "==3.1.*"
|
||||||
py7zrversion: ">=0.20.2"
|
py7zrversion: ">=0.20.2"
|
||||||
version: ${{ matrix.qt_version }}
|
version: ${{ matrix.qt_version }}
|
||||||
host: "windows"
|
host: "windows"
|
||||||
target: "desktop"
|
target: "desktop"
|
||||||
arch: ""
|
arch: ${{ matrix.qt_arch }}
|
||||||
modules: ${{ matrix.qt_modules }}
|
modules: ${{ matrix.qt_modules }}
|
||||||
cache: ${{ inputs.is_qt_cached }}
|
cache: ${{ inputs.is_qt_cached }}
|
||||||
cache-key-prefix: host-qt-arm64-windows
|
cache-key-prefix: host-qt-arm64-windows
|
||||||
@ -232,7 +232,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install Qt (macOS, Linux & Windows MSVC)
|
- name: Install Qt (macOS, Linux & Windows MSVC)
|
||||||
if: matrix.msystem == ''
|
if: matrix.msystem == ''
|
||||||
uses: jurplel/install-qt-action@v3
|
uses: jurplel/install-qt-action@v4
|
||||||
with:
|
with:
|
||||||
aqtversion: "==3.1.*"
|
aqtversion: "==3.1.*"
|
||||||
py7zrversion: ">=0.20.2"
|
py7zrversion: ">=0.20.2"
|
||||||
@ -259,12 +259,12 @@ jobs:
|
|||||||
|
|
||||||
wget "https://github.com/AppImageCommunity/AppImageUpdate/releases/download/continuous/AppImageUpdate-x86_64.AppImage"
|
wget "https://github.com/AppImageCommunity/AppImageUpdate/releases/download/continuous/AppImageUpdate-x86_64.AppImage"
|
||||||
|
|
||||||
sudo apt install libopengl0
|
sudo apt install libopengl0 libfuse2
|
||||||
|
|
||||||
- name: Add QT_HOST_PATH var (Windows MSVC arm64)
|
- name: Add QT_HOST_PATH var (Windows MSVC arm64)
|
||||||
if: runner.os == 'Windows' && matrix.architecture == 'arm64'
|
if: runner.os == 'Windows' && matrix.architecture == 'arm64'
|
||||||
run: |
|
run: |
|
||||||
echo "QT_HOST_PATH=${{ github.workspace }}\HostQt\Qt\${{ matrix.qt_version }}\msvc2019_64" >> $env:GITHUB_ENV
|
echo "QT_HOST_PATH=${{ github.workspace }}\HostQt\Qt\${{ matrix.qt_version }}\msvc2022_64" >> $env:GITHUB_ENV
|
||||||
|
|
||||||
- name: Setup java (macOS)
|
- name: Setup java (macOS)
|
||||||
if: runner.os == 'macOS'
|
if: runner.os == 'macOS'
|
||||||
@ -380,11 +380,13 @@ jobs:
|
|||||||
|
|
||||||
if [ -n '${{ secrets.APPLE_CODESIGN_ID }}' ]; then
|
if [ -n '${{ secrets.APPLE_CODESIGN_ID }}' ]; then
|
||||||
APPLE_CODESIGN_ID='${{ secrets.APPLE_CODESIGN_ID }}'
|
APPLE_CODESIGN_ID='${{ secrets.APPLE_CODESIGN_ID }}'
|
||||||
|
ENTITLEMENTS_FILE='../program_info/App.entitlements'
|
||||||
else
|
else
|
||||||
APPLE_CODESIGN_ID='-'
|
APPLE_CODESIGN_ID='-'
|
||||||
|
ENTITLEMENTS_FILE='../program_info/AdhocSignedApp.entitlements'
|
||||||
fi
|
fi
|
||||||
|
|
||||||
sudo codesign --sign "$APPLE_CODESIGN_ID" --deep --force --entitlements "../program_info/App.entitlements" --options runtime "PrismLauncher.app/Contents/MacOS/prismlauncher"
|
sudo codesign --sign "$APPLE_CODESIGN_ID" --deep --force --entitlements "$ENTITLEMENTS_FILE" --options runtime "PrismLauncher.app/Contents/MacOS/prismlauncher"
|
||||||
mv "PrismLauncher.app" "Prism Launcher.app"
|
mv "PrismLauncher.app" "Prism Launcher.app"
|
||||||
|
|
||||||
- name: Notarize (macOS)
|
- name: Notarize (macOS)
|
||||||
@ -519,8 +521,8 @@ jobs:
|
|||||||
|
|
||||||
cp -r ${{ runner.workspace }}/Qt/${{ matrix.qt_version }}/gcc_64/plugins/iconengines/* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/plugins/iconengines
|
cp -r ${{ runner.workspace }}/Qt/${{ matrix.qt_version }}/gcc_64/plugins/iconengines/* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/plugins/iconengines
|
||||||
|
|
||||||
cp /usr/lib/x86_64-linux-gnu/libcrypto.so.1.1 ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/
|
cp /usr/lib/x86_64-linux-gnu/libcrypto.so.* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/
|
||||||
cp /usr/lib/x86_64-linux-gnu/libssl.so.1.1 ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/
|
cp /usr/lib/x86_64-linux-gnu/libssl.so.* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/
|
||||||
cp /usr/lib/x86_64-linux-gnu/libOpenGL.so.0* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/
|
cp /usr/lib/x86_64-linux-gnu/libOpenGL.so.0* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/
|
||||||
|
|
||||||
LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib"
|
LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib"
|
||||||
@ -555,9 +557,9 @@ jobs:
|
|||||||
mkdir ${{ env.INSTALL_PORTABLE_DIR }}/lib
|
mkdir ${{ env.INSTALL_PORTABLE_DIR }}/lib
|
||||||
cp /lib/x86_64-linux-gnu/libbz2.so.1.0 ${{ env.INSTALL_PORTABLE_DIR }}/lib
|
cp /lib/x86_64-linux-gnu/libbz2.so.1.0 ${{ env.INSTALL_PORTABLE_DIR }}/lib
|
||||||
cp /usr/lib/x86_64-linux-gnu/libgobject-2.0.so.0 ${{ env.INSTALL_PORTABLE_DIR }}/lib
|
cp /usr/lib/x86_64-linux-gnu/libgobject-2.0.so.0 ${{ env.INSTALL_PORTABLE_DIR }}/lib
|
||||||
cp /usr/lib/x86_64-linux-gnu/libcrypto.so.1.1 ${{ env.INSTALL_PORTABLE_DIR }}/lib
|
cp /usr/lib/x86_64-linux-gnu/libcrypto.so.* ${{ env.INSTALL_PORTABLE_DIR }}/lib
|
||||||
cp /usr/lib/x86_64-linux-gnu/libssl.so.1.1 ${{ env.INSTALL_PORTABLE_DIR }}/lib
|
cp /usr/lib/x86_64-linux-gnu/libssl.so.* ${{ env.INSTALL_PORTABLE_DIR }}/lib
|
||||||
cp /usr/lib/x86_64-linux-gnu/libffi.so.7 ${{ env.INSTALL_PORTABLE_DIR }}/lib
|
cp /usr/lib/x86_64-linux-gnu/libffi.so.*.* ${{ env.INSTALL_PORTABLE_DIR }}/lib
|
||||||
mv ${{ env.INSTALL_PORTABLE_DIR }}/bin/*.so* ${{ env.INSTALL_PORTABLE_DIR }}/lib
|
mv ${{ env.INSTALL_PORTABLE_DIR }}/bin/*.so* ${{ 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
|
||||||
@ -631,22 +633,42 @@ jobs:
|
|||||||
ccache -s
|
ccache -s
|
||||||
|
|
||||||
flatpak:
|
flatpak:
|
||||||
runs-on: ubuntu-latest
|
name: Flatpak (${{ matrix.arch }})
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: ubuntu-22.04
|
||||||
|
arch: x86_64
|
||||||
|
|
||||||
|
- os: ubuntu-22.04-arm
|
||||||
|
arch: aarch64
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
container:
|
container:
|
||||||
image: bilelmoussaoui/flatpak-github-actions:kde-6.7
|
image: ghcr.io/flathub-infra/flatpak-github-actions:kde-6.8
|
||||||
options: --privileged
|
options: --privileged
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
if: inputs.build_type == 'Debug'
|
if: inputs.build_type == 'Debug'
|
||||||
with:
|
with:
|
||||||
submodules: "true"
|
submodules: true
|
||||||
|
|
||||||
|
- name: Set short version
|
||||||
|
shell: bash
|
||||||
|
run: echo "VERSION=${GITHUB_SHA::7}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Build Flatpak (Linux)
|
- name: Build Flatpak (Linux)
|
||||||
if: inputs.build_type == 'Debug'
|
if: inputs.build_type == 'Debug'
|
||||||
uses: flatpak/flatpak-github-actions/flatpak-builder@v6
|
uses: flatpak/flatpak-github-actions/flatpak-builder@v6
|
||||||
with:
|
with:
|
||||||
bundle: "Prism Launcher.flatpak"
|
bundle: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-Flatpak.flatpak
|
||||||
manifest-path: flatpak/org.prismlauncher.PrismLauncher.yml
|
manifest-path: flatpak/org.prismlauncher.PrismLauncher.yml
|
||||||
|
arch: ${{ matrix.arch }}
|
||||||
|
|
||||||
nix:
|
nix:
|
||||||
name: Nix (${{ matrix.system }})
|
name: Nix (${{ matrix.system }})
|
||||||
@ -658,6 +680,9 @@ jobs:
|
|||||||
- os: ubuntu-22.04
|
- os: ubuntu-22.04
|
||||||
system: x86_64-linux
|
system: x86_64-linux
|
||||||
|
|
||||||
|
- os: ubuntu-22.04-arm
|
||||||
|
system: aarch64-linux
|
||||||
|
|
||||||
- os: macos-13
|
- os: macos-13
|
||||||
system: x86_64-darwin
|
system: x86_64-darwin
|
||||||
|
|
||||||
|
@ -78,6 +78,13 @@ else()
|
|||||||
# ATL's pack list needs more than the default 1 Mib stack on windows
|
# ATL's pack list needs more than the default 1 Mib stack on windows
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
set(CMAKE_EXE_LINKER_FLAGS "-Wl,--stack,8388608 ${CMAKE_EXE_LINKER_FLAGS}")
|
set(CMAKE_EXE_LINKER_FLAGS "-Wl,--stack,8388608 ${CMAKE_EXE_LINKER_FLAGS}")
|
||||||
|
|
||||||
|
# -ffunction-sections and -fdata-sections help reduce binary size
|
||||||
|
# -mguard=cf enables Control Flow Guard
|
||||||
|
# TODO: Look into -gc-sections to further reduce binary size
|
||||||
|
foreach(lang C CXX)
|
||||||
|
set("CMAKE_${lang}_FLAGS_RELEASE" "-ffunction-sections -fdata-sections -mguard=cf")
|
||||||
|
endforeach()
|
||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
@ -401,8 +408,8 @@ if(UNIX AND APPLE)
|
|||||||
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.5.2/Sparkle-2.5.2.tar.xz" CACHE STRING "URL to Sparkle release archive")
|
set(MACOSX_SPARKLE_DOWNLOAD_URL "https://github.com/sparkle-project/Sparkle/releases/download/2.6.4/Sparkle-2.6.4.tar.xz" CACHE STRING "URL to Sparkle release archive")
|
||||||
set(MACOSX_SPARKLE_SHA256 "572dd67ae398a466f19f343a449e1890bac1ef74885b4739f68f979a8a89884b" CACHE STRING "SHA256 checksum for Sparkle release archive")
|
set(MACOSX_SPARKLE_SHA256 "50612a06038abc931f16011d7903b8326a362c1074dabccb718404ce8e585f0b" 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
|
||||||
|
@ -8,6 +8,8 @@
|
|||||||
<string>A Minecraft mod wants to access your microphone.</string>
|
<string>A Minecraft mod wants to access your microphone.</string>
|
||||||
<key>NSDownloadsFolderUsageDescription</key>
|
<key>NSDownloadsFolderUsageDescription</key>
|
||||||
<string>Prism uses access to your Downloads folder to help you more quickly add mods that can't be automatically downloaded to your instance. You can change where Prism scans for downloaded mods in Settings or the prompt that appears.</string>
|
<string>Prism uses access to your Downloads folder to help you more quickly add mods that can't be automatically downloaded to your instance. You can change where Prism scans for downloaded mods in Settings or the prompt that appears.</string>
|
||||||
|
<key>NSLocalNetworkUsageDescription</key>
|
||||||
|
<string>Minecraft uses the local network to find and connect to LAN servers.</string>
|
||||||
<key>NSPrincipalClass</key>
|
<key>NSPrincipalClass</key>
|
||||||
<string>NSApplication</string>
|
<string>NSApplication</string>
|
||||||
<key>NSHighResolutionCapable</key>
|
<key>NSHighResolutionCapable</key>
|
||||||
|
12
flake.lock
generated
12
flake.lock
generated
@ -3,11 +3,11 @@
|
|||||||
"flake-compat": {
|
"flake-compat": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1696426674,
|
"lastModified": 1733328505,
|
||||||
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
|
"narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=",
|
||||||
"owner": "edolstra",
|
"owner": "edolstra",
|
||||||
"repo": "flake-compat",
|
"repo": "flake-compat",
|
||||||
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
|
"rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@ -49,11 +49,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1732014248,
|
"lastModified": 1737062831,
|
||||||
"narHash": "sha256-y/MEyuJ5oBWrWAic/14LaIr/u5E0wRVzyYsouYY3W6w=",
|
"narHash": "sha256-Tbk1MZbtV2s5aG+iM99U8FqwxU/YNArMcWAv6clcsBc=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "23e89b7da85c3640bbc2173fe04f4bd114342367",
|
"rev": "5df43628fdf08d642be8ba5b3625a6c70731c19c",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
id: org.prismlauncher.PrismLauncher
|
id: org.prismlauncher.PrismLauncher
|
||||||
runtime: org.kde.Platform
|
runtime: org.kde.Platform
|
||||||
runtime-version: '6.7'
|
runtime-version: '6.8'
|
||||||
sdk: org.kde.Sdk
|
sdk: org.kde.Sdk
|
||||||
sdk-extensions:
|
sdk-extensions:
|
||||||
- org.freedesktop.Sdk.Extension.openjdk17
|
- org.freedesktop.Sdk.Extension.openjdk17
|
||||||
@ -75,8 +75,8 @@ modules:
|
|||||||
buildsystem: autotools
|
buildsystem: autotools
|
||||||
sources:
|
sources:
|
||||||
- type: archive
|
- type: archive
|
||||||
url: https://xorg.freedesktop.org/archive/individual/app/xrandr-1.5.2.tar.xz
|
url: https://xorg.freedesktop.org/archive/individual/app/xrandr-1.5.3.tar.xz
|
||||||
sha256: c8bee4790d9058bacc4b6246456c58021db58a87ddda1a9d0139bf5f18f1f240
|
sha256: f8dd7566adb74147fab9964680b6bbadee87cf406a7fcff51718a5e6949b841c
|
||||||
x-checker-data:
|
x-checker-data:
|
||||||
type: anitya
|
type: anitya
|
||||||
project-id: 14957
|
project-id: 14957
|
||||||
|
@ -614,6 +614,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
|
|||||||
m_settings->registerSetting("IconsDir", "icons");
|
m_settings->registerSetting("IconsDir", "icons");
|
||||||
m_settings->registerSetting("DownloadsDir", QStandardPaths::writableLocation(QStandardPaths::DownloadLocation));
|
m_settings->registerSetting("DownloadsDir", QStandardPaths::writableLocation(QStandardPaths::DownloadLocation));
|
||||||
m_settings->registerSetting("DownloadsDirWatchRecursive", false);
|
m_settings->registerSetting("DownloadsDirWatchRecursive", false);
|
||||||
|
m_settings->registerSetting("MoveModsFromDownloadsDir", false);
|
||||||
m_settings->registerSetting("SkinsDir", "skins");
|
m_settings->registerSetting("SkinsDir", "skins");
|
||||||
m_settings->registerSetting("JavaDir", "java");
|
m_settings->registerSetting("JavaDir", "java");
|
||||||
|
|
||||||
|
@ -1024,8 +1024,6 @@ SET(LAUNCHER_SOURCES
|
|||||||
ui/dialogs/CopyInstanceDialog.h
|
ui/dialogs/CopyInstanceDialog.h
|
||||||
ui/dialogs/CustomMessageBox.cpp
|
ui/dialogs/CustomMessageBox.cpp
|
||||||
ui/dialogs/CustomMessageBox.h
|
ui/dialogs/CustomMessageBox.h
|
||||||
ui/dialogs/EditAccountDialog.cpp
|
|
||||||
ui/dialogs/EditAccountDialog.h
|
|
||||||
ui/dialogs/ExportInstanceDialog.cpp
|
ui/dialogs/ExportInstanceDialog.cpp
|
||||||
ui/dialogs/ExportInstanceDialog.h
|
ui/dialogs/ExportInstanceDialog.h
|
||||||
ui/dialogs/ExportPackDialog.cpp
|
ui/dialogs/ExportPackDialog.cpp
|
||||||
@ -1079,8 +1077,6 @@ SET(LAUNCHER_SOURCES
|
|||||||
ui/widgets/CustomCommands.h
|
ui/widgets/CustomCommands.h
|
||||||
ui/widgets/EnvironmentVariables.cpp
|
ui/widgets/EnvironmentVariables.cpp
|
||||||
ui/widgets/EnvironmentVariables.h
|
ui/widgets/EnvironmentVariables.h
|
||||||
ui/widgets/DropLabel.cpp
|
|
||||||
ui/widgets/DropLabel.h
|
|
||||||
ui/widgets/FocusLineEdit.cpp
|
ui/widgets/FocusLineEdit.cpp
|
||||||
ui/widgets/FocusLineEdit.h
|
ui/widgets/FocusLineEdit.h
|
||||||
ui/widgets/IconLabel.cpp
|
ui/widgets/IconLabel.cpp
|
||||||
@ -1215,7 +1211,6 @@ qt_wrap_ui(LAUNCHER_UI
|
|||||||
ui/dialogs/MSALoginDialog.ui
|
ui/dialogs/MSALoginDialog.ui
|
||||||
ui/dialogs/OfflineLoginDialog.ui
|
ui/dialogs/OfflineLoginDialog.ui
|
||||||
ui/dialogs/AboutDialog.ui
|
ui/dialogs/AboutDialog.ui
|
||||||
ui/dialogs/EditAccountDialog.ui
|
|
||||||
ui/dialogs/ReviewMessageBox.ui
|
ui/dialogs/ReviewMessageBox.ui
|
||||||
ui/dialogs/ScrollMessageBox.ui
|
ui/dialogs/ScrollMessageBox.ui
|
||||||
ui/dialogs/BlockedModsDialog.ui
|
ui/dialogs/BlockedModsDialog.ui
|
||||||
|
@ -47,24 +47,24 @@
|
|||||||
|
|
||||||
#define MAX_SIZE 1024
|
#define MAX_SIZE 1024
|
||||||
|
|
||||||
IconList::IconList(const QStringList& builtinPaths, QString path, QObject* parent) : QAbstractListModel(parent)
|
IconList::IconList(const QStringList& builtinPaths, const QString& path, QObject* parent) : QAbstractListModel(parent)
|
||||||
{
|
{
|
||||||
QSet<QString> builtinNames;
|
QSet<QString> builtinNames;
|
||||||
|
|
||||||
// add builtin icons
|
// add builtin icons
|
||||||
for (auto& builtinPath : builtinPaths) {
|
for (const auto& builtinPath : builtinPaths) {
|
||||||
QDir instance_icons(builtinPath);
|
QDir instanceIcons(builtinPath);
|
||||||
auto file_info_list = instance_icons.entryInfoList(QDir::Files, QDir::Name);
|
auto fileInfoList = instanceIcons.entryInfoList(QDir::Files, QDir::Name);
|
||||||
for (auto file_info : file_info_list) {
|
for (const auto& fileInfo : fileInfoList) {
|
||||||
builtinNames.insert(file_info.completeBaseName());
|
builtinNames.insert(fileInfo.baseName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (auto& builtinName : builtinNames) {
|
for (const auto& builtinName : builtinNames) {
|
||||||
addThemeIcon(builtinName);
|
addThemeIcon(builtinName);
|
||||||
}
|
}
|
||||||
|
|
||||||
m_watcher.reset(new QFileSystemWatcher());
|
m_watcher.reset(new QFileSystemWatcher());
|
||||||
is_watching = false;
|
m_isWatching = false;
|
||||||
connect(m_watcher.get(), &QFileSystemWatcher::directoryChanged, this, &IconList::directoryChanged);
|
connect(m_watcher.get(), &QFileSystemWatcher::directoryChanged, this, &IconList::directoryChanged);
|
||||||
connect(m_watcher.get(), &QFileSystemWatcher::fileChanged, this, &IconList::fileChanged);
|
connect(m_watcher.get(), &QFileSystemWatcher::fileChanged, this, &IconList::fileChanged);
|
||||||
|
|
||||||
@ -77,91 +77,131 @@ IconList::IconList(const QStringList& builtinPaths, QString path, QObject* paren
|
|||||||
void IconList::sortIconList()
|
void IconList::sortIconList()
|
||||||
{
|
{
|
||||||
qDebug() << "Sorting icon list...";
|
qDebug() << "Sorting icon list...";
|
||||||
std::sort(icons.begin(), icons.end(), [](const MMCIcon& a, const MMCIcon& b) { return a.m_key.localeAwareCompare(b.m_key) < 0; });
|
std::sort(m_icons.begin(), m_icons.end(), [](const MMCIcon& a, const MMCIcon& b) {
|
||||||
|
bool aIsSubdir = a.m_key.contains(QDir::separator());
|
||||||
|
bool bIsSubdir = b.m_key.contains(QDir::separator());
|
||||||
|
if (aIsSubdir != bIsSubdir) {
|
||||||
|
return !aIsSubdir; // root-level icons come first
|
||||||
|
}
|
||||||
|
return a.m_key.localeAwareCompare(b.m_key) < 0;
|
||||||
|
});
|
||||||
reindex();
|
reindex();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to add directories recursively
|
||||||
|
bool IconList::addPathRecursively(const QString& path)
|
||||||
|
{
|
||||||
|
QDir dir(path);
|
||||||
|
if (!dir.exists())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Add the directory itself
|
||||||
|
bool watching = m_watcher->addPath(path);
|
||||||
|
|
||||||
|
// Add all subdirectories
|
||||||
|
QFileInfoList entries = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot);
|
||||||
|
for (const QFileInfo& entry : entries) {
|
||||||
|
if (addPathRecursively(entry.absoluteFilePath())) {
|
||||||
|
watching = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return watching;
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList IconList::getIconFilePaths() const
|
||||||
|
{
|
||||||
|
QStringList iconFiles{};
|
||||||
|
QStringList directories{ m_dir.absolutePath() };
|
||||||
|
while (!directories.isEmpty()) {
|
||||||
|
QString first = directories.takeFirst();
|
||||||
|
QDir dir(first);
|
||||||
|
for (QFileInfo& fileInfo : dir.entryInfoList(QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot, QDir::Name)) {
|
||||||
|
if (fileInfo.isDir())
|
||||||
|
directories.push_back(fileInfo.absoluteFilePath());
|
||||||
|
else
|
||||||
|
iconFiles.push_back(fileInfo.absoluteFilePath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return iconFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString formatName(const QDir& iconsDir, const QFileInfo& iconFile)
|
||||||
|
{
|
||||||
|
if (iconFile.dir() == iconsDir)
|
||||||
|
return iconFile.baseName();
|
||||||
|
|
||||||
|
constexpr auto delimiter = " » ";
|
||||||
|
QString relativePathWithoutExtension = iconsDir.relativeFilePath(iconFile.dir().path()) + QDir::separator() + iconFile.baseName();
|
||||||
|
return relativePathWithoutExtension.replace(QDir::separator(), delimiter);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Split into a separate function because the preprocessing impedes readability
|
||||||
|
QSet<QString> toStringSet(const QList<QString>& list)
|
||||||
|
{
|
||||||
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
|
||||||
|
QSet<QString> set(list.begin(), list.end());
|
||||||
|
#else
|
||||||
|
QSet<QString> set = list.toSet();
|
||||||
|
#endif
|
||||||
|
return set;
|
||||||
|
}
|
||||||
|
|
||||||
void IconList::directoryChanged(const QString& path)
|
void IconList::directoryChanged(const QString& path)
|
||||||
{
|
{
|
||||||
QDir new_dir(path);
|
QDir newDir(path);
|
||||||
if (m_dir.absolutePath() != new_dir.absolutePath()) {
|
if (m_dir.absolutePath() != newDir.absolutePath()) {
|
||||||
m_dir.setPath(path);
|
if (!path.startsWith(m_dir.absolutePath()))
|
||||||
|
m_dir.setPath(path);
|
||||||
m_dir.refresh();
|
m_dir.refresh();
|
||||||
if (is_watching)
|
if (m_isWatching)
|
||||||
stopWatching();
|
stopWatching();
|
||||||
startWatching();
|
startWatching();
|
||||||
}
|
}
|
||||||
if (!m_dir.exists())
|
if (!m_dir.exists() && !FS::ensureFolderPathExists(m_dir.absolutePath()))
|
||||||
if (!FS::ensureFolderPathExists(m_dir.absolutePath()))
|
return;
|
||||||
return;
|
|
||||||
m_dir.refresh();
|
m_dir.refresh();
|
||||||
auto new_list = m_dir.entryList(QDir::Files, QDir::Name);
|
const QStringList newFileNamesList = getIconFilePaths();
|
||||||
for (auto it = new_list.begin(); it != new_list.end(); it++) {
|
const QSet<QString> newSet = toStringSet(newFileNamesList);
|
||||||
QString& foo = (*it);
|
QSet<QString> currentSet;
|
||||||
foo = m_dir.filePath(foo);
|
for (const MMCIcon& it : m_icons) {
|
||||||
}
|
|
||||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
|
|
||||||
QSet<QString> new_set(new_list.begin(), new_list.end());
|
|
||||||
#else
|
|
||||||
auto new_set = new_list.toSet();
|
|
||||||
#endif
|
|
||||||
QList<QString> current_list;
|
|
||||||
for (auto& it : icons) {
|
|
||||||
if (!it.has(IconType::FileBased))
|
if (!it.has(IconType::FileBased))
|
||||||
continue;
|
continue;
|
||||||
current_list.push_back(it.m_images[IconType::FileBased].filename);
|
currentSet.insert(it.m_images[IconType::FileBased].filename);
|
||||||
}
|
}
|
||||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
|
QSet<QString> toRemove = currentSet - newSet;
|
||||||
QSet<QString> current_set(current_list.begin(), current_list.end());
|
QSet<QString> toAdd = newSet - currentSet;
|
||||||
#else
|
|
||||||
QSet<QString> current_set = current_list.toSet();
|
|
||||||
#endif
|
|
||||||
|
|
||||||
QSet<QString> to_remove = current_set;
|
for (const QString& removedPath : toRemove) {
|
||||||
to_remove -= new_set;
|
qDebug() << "Removing icon " << removedPath;
|
||||||
|
QFileInfo removedFile(removedPath);
|
||||||
QSet<QString> to_add = new_set;
|
QString key = m_dir.relativeFilePath(removedFile.absoluteFilePath());
|
||||||
to_add -= current_set;
|
|
||||||
|
|
||||||
for (auto remove : to_remove) {
|
|
||||||
qDebug() << "Removing " << remove;
|
|
||||||
QFileInfo rmfile(remove);
|
|
||||||
QString key = rmfile.completeBaseName();
|
|
||||||
|
|
||||||
QString suffix = rmfile.suffix();
|
|
||||||
// The icon doesnt have a suffix, but it can have other .s in the name, so we account for those as well
|
|
||||||
if (!IconUtils::isIconSuffix(suffix))
|
|
||||||
key = rmfile.fileName();
|
|
||||||
|
|
||||||
int idx = getIconIndex(key);
|
int idx = getIconIndex(key);
|
||||||
if (idx == -1)
|
if (idx == -1)
|
||||||
continue;
|
continue;
|
||||||
icons[idx].remove(IconType::FileBased);
|
m_icons[idx].remove(FileBased);
|
||||||
if (icons[idx].type() == IconType::ToBeDeleted) {
|
if (m_icons[idx].type() == ToBeDeleted) {
|
||||||
beginRemoveRows(QModelIndex(), idx, idx);
|
beginRemoveRows(QModelIndex(), idx, idx);
|
||||||
icons.remove(idx);
|
m_icons.remove(idx);
|
||||||
reindex();
|
reindex();
|
||||||
endRemoveRows();
|
endRemoveRows();
|
||||||
} else {
|
} else {
|
||||||
dataChanged(index(idx), index(idx));
|
dataChanged(index(idx), index(idx));
|
||||||
}
|
}
|
||||||
m_watcher->removePath(remove);
|
m_watcher->removePath(removedPath);
|
||||||
emit iconUpdated(key);
|
emit iconUpdated(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (auto add : to_add) {
|
for (const QString& addedPath : toAdd) {
|
||||||
qDebug() << "Adding " << add;
|
qDebug() << "Adding icon " << addedPath;
|
||||||
|
|
||||||
QFileInfo addfile(add);
|
QFileInfo addfile(addedPath);
|
||||||
QString key = addfile.completeBaseName();
|
QString relativePath = m_dir.relativeFilePath(addfile.absoluteFilePath());
|
||||||
|
QString key = QFileInfo(relativePath).completeBaseName();
|
||||||
|
QString name = formatName(m_dir, addfile);
|
||||||
|
|
||||||
QString suffix = addfile.suffix();
|
if (addIcon(key, name, addfile.filePath(), IconType::FileBased)) {
|
||||||
// The icon doesnt have a suffix, but it can have other .s in the name, so we account for those as well
|
m_watcher->addPath(addedPath);
|
||||||
if (!IconUtils::isIconSuffix(suffix))
|
|
||||||
key = addfile.fileName();
|
|
||||||
|
|
||||||
if (addIcon(key, QString(), addfile.filePath(), IconType::FileBased)) {
|
|
||||||
m_watcher->addPath(add);
|
|
||||||
emit iconUpdated(key);
|
emit iconUpdated(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -171,24 +211,24 @@ void IconList::directoryChanged(const QString& path)
|
|||||||
|
|
||||||
void IconList::fileChanged(const QString& path)
|
void IconList::fileChanged(const QString& path)
|
||||||
{
|
{
|
||||||
qDebug() << "Checking " << path;
|
qDebug() << "Checking icon " << path;
|
||||||
QFileInfo checkfile(path);
|
QFileInfo checkfile(path);
|
||||||
if (!checkfile.exists())
|
if (!checkfile.exists())
|
||||||
return;
|
return;
|
||||||
QString key = checkfile.completeBaseName();
|
QString key = m_dir.relativeFilePath(checkfile.absoluteFilePath());
|
||||||
int idx = getIconIndex(key);
|
int idx = getIconIndex(key);
|
||||||
if (idx == -1)
|
if (idx == -1)
|
||||||
return;
|
return;
|
||||||
QIcon icon(path);
|
QIcon icon(path);
|
||||||
if (!icon.availableSizes().size())
|
if (icon.availableSizes().empty())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
icons[idx].m_images[IconType::FileBased].icon = icon;
|
m_icons[idx].m_images[IconType::FileBased].icon = icon;
|
||||||
dataChanged(index(idx), index(idx));
|
dataChanged(index(idx), index(idx));
|
||||||
emit iconUpdated(key);
|
emit iconUpdated(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
void IconList::SettingChanged(const Setting& setting, QVariant value)
|
void IconList::SettingChanged(const Setting& setting, const QVariant& value)
|
||||||
{
|
{
|
||||||
if (setting.id() != "IconsDir")
|
if (setting.id() != "IconsDir")
|
||||||
return;
|
return;
|
||||||
@ -200,8 +240,8 @@ void IconList::startWatching()
|
|||||||
{
|
{
|
||||||
auto abs_path = m_dir.absolutePath();
|
auto abs_path = m_dir.absolutePath();
|
||||||
FS::ensureFolderPathExists(abs_path);
|
FS::ensureFolderPathExists(abs_path);
|
||||||
is_watching = m_watcher->addPath(abs_path);
|
m_isWatching = addPathRecursively(abs_path);
|
||||||
if (is_watching) {
|
if (m_isWatching) {
|
||||||
qDebug() << "Started watching " << abs_path;
|
qDebug() << "Started watching " << abs_path;
|
||||||
} else {
|
} else {
|
||||||
qDebug() << "Failed to start watching " << abs_path;
|
qDebug() << "Failed to start watching " << abs_path;
|
||||||
@ -212,7 +252,7 @@ void IconList::stopWatching()
|
|||||||
{
|
{
|
||||||
m_watcher->removePaths(m_watcher->files());
|
m_watcher->removePaths(m_watcher->files());
|
||||||
m_watcher->removePaths(m_watcher->directories());
|
m_watcher->removePaths(m_watcher->directories());
|
||||||
is_watching = false;
|
m_isWatching = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
QStringList IconList::mimeTypes() const
|
QStringList IconList::mimeTypes() const
|
||||||
@ -242,7 +282,7 @@ bool IconList::dropMimeData(const QMimeData* data,
|
|||||||
if (data->hasUrls()) {
|
if (data->hasUrls()) {
|
||||||
auto urls = data->urls();
|
auto urls = data->urls();
|
||||||
QStringList iconFiles;
|
QStringList iconFiles;
|
||||||
for (auto url : urls) {
|
for (const auto& url : urls) {
|
||||||
// only local files may be dropped...
|
// only local files may be dropped...
|
||||||
if (!url.isLocalFile())
|
if (!url.isLocalFile())
|
||||||
continue;
|
continue;
|
||||||
@ -263,33 +303,33 @@ Qt::ItemFlags IconList::flags(const QModelIndex& index) const
|
|||||||
QVariant IconList::data(const QModelIndex& index, int role) const
|
QVariant IconList::data(const QModelIndex& index, int role) const
|
||||||
{
|
{
|
||||||
if (!index.isValid())
|
if (!index.isValid())
|
||||||
return QVariant();
|
return {};
|
||||||
|
|
||||||
int row = index.row();
|
int row = index.row();
|
||||||
|
|
||||||
if (row < 0 || row >= icons.size())
|
if (row < 0 || row >= m_icons.size())
|
||||||
return QVariant();
|
return {};
|
||||||
|
|
||||||
switch (role) {
|
switch (role) {
|
||||||
case Qt::DecorationRole:
|
case Qt::DecorationRole:
|
||||||
return icons[row].icon();
|
return m_icons[row].icon();
|
||||||
case Qt::DisplayRole:
|
case Qt::DisplayRole:
|
||||||
return icons[row].name();
|
return m_icons[row].name();
|
||||||
case Qt::UserRole:
|
case Qt::UserRole:
|
||||||
return icons[row].m_key;
|
return m_icons[row].m_key;
|
||||||
default:
|
default:
|
||||||
return QVariant();
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int IconList::rowCount(const QModelIndex& parent) const
|
int IconList::rowCount(const QModelIndex& parent) const
|
||||||
{
|
{
|
||||||
return parent.isValid() ? 0 : icons.size();
|
return parent.isValid() ? 0 : m_icons.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
void IconList::installIcons(const QStringList& iconFiles)
|
void IconList::installIcons(const QStringList& iconFiles)
|
||||||
{
|
{
|
||||||
for (QString file : iconFiles)
|
for (const QString& file : iconFiles)
|
||||||
installIcon(file, {});
|
installIcon(file, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -312,12 +352,13 @@ bool IconList::iconFileExists(const QString& key) const
|
|||||||
return iconEntry && iconEntry->has(IconType::FileBased);
|
return iconEntry && iconEntry->has(IconType::FileBased);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the icon with the given key or nullptr if it doesn't exist.
|
||||||
const MMCIcon* IconList::icon(const QString& key) const
|
const MMCIcon* IconList::icon(const QString& key) const
|
||||||
{
|
{
|
||||||
int iconIdx = getIconIndex(key);
|
int iconIdx = getIconIndex(key);
|
||||||
if (iconIdx == -1)
|
if (iconIdx == -1)
|
||||||
return nullptr;
|
return nullptr;
|
||||||
return &icons[iconIdx];
|
return &m_icons[iconIdx];
|
||||||
}
|
}
|
||||||
|
|
||||||
bool IconList::deleteIcon(const QString& key)
|
bool IconList::deleteIcon(const QString& key)
|
||||||
@ -332,22 +373,22 @@ bool IconList::trashIcon(const QString& key)
|
|||||||
|
|
||||||
bool IconList::addThemeIcon(const QString& key)
|
bool IconList::addThemeIcon(const QString& key)
|
||||||
{
|
{
|
||||||
auto iter = name_index.find(key);
|
auto iter = m_nameIndex.find(key);
|
||||||
if (iter != name_index.end()) {
|
if (iter != m_nameIndex.end()) {
|
||||||
auto& oldOne = icons[*iter];
|
auto& oldOne = m_icons[*iter];
|
||||||
oldOne.replace(Builtin, key);
|
oldOne.replace(Builtin, key);
|
||||||
dataChanged(index(*iter), index(*iter));
|
dataChanged(index(*iter), index(*iter));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// add a new icon
|
// add a new icon
|
||||||
beginInsertRows(QModelIndex(), icons.size(), icons.size());
|
beginInsertRows(QModelIndex(), m_icons.size(), m_icons.size());
|
||||||
{
|
{
|
||||||
MMCIcon mmc_icon;
|
MMCIcon mmc_icon;
|
||||||
mmc_icon.m_name = key;
|
mmc_icon.m_name = key;
|
||||||
mmc_icon.m_key = key;
|
mmc_icon.m_key = key;
|
||||||
mmc_icon.replace(Builtin, key);
|
mmc_icon.replace(Builtin, key);
|
||||||
icons.push_back(mmc_icon);
|
m_icons.push_back(mmc_icon);
|
||||||
name_index[key] = icons.size() - 1;
|
m_nameIndex[key] = m_icons.size() - 1;
|
||||||
}
|
}
|
||||||
endInsertRows();
|
endInsertRows();
|
||||||
return true;
|
return true;
|
||||||
@ -359,22 +400,22 @@ bool IconList::addIcon(const QString& key, const QString& name, const QString& p
|
|||||||
QIcon icon(path);
|
QIcon icon(path);
|
||||||
if (icon.isNull())
|
if (icon.isNull())
|
||||||
return false;
|
return false;
|
||||||
auto iter = name_index.find(key);
|
auto iter = m_nameIndex.find(key);
|
||||||
if (iter != name_index.end()) {
|
if (iter != m_nameIndex.end()) {
|
||||||
auto& oldOne = icons[*iter];
|
auto& oldOne = m_icons[*iter];
|
||||||
oldOne.replace(type, icon, path);
|
oldOne.replace(type, icon, path);
|
||||||
dataChanged(index(*iter), index(*iter));
|
dataChanged(index(*iter), index(*iter));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// add a new icon
|
// add a new icon
|
||||||
beginInsertRows(QModelIndex(), icons.size(), icons.size());
|
beginInsertRows(QModelIndex(), m_icons.size(), m_icons.size());
|
||||||
{
|
{
|
||||||
MMCIcon mmc_icon;
|
MMCIcon mmc_icon;
|
||||||
mmc_icon.m_name = name;
|
mmc_icon.m_name = name;
|
||||||
mmc_icon.m_key = key;
|
mmc_icon.m_key = key;
|
||||||
mmc_icon.replace(type, icon, path);
|
mmc_icon.replace(type, icon, path);
|
||||||
icons.push_back(mmc_icon);
|
m_icons.push_back(mmc_icon);
|
||||||
name_index[key] = icons.size() - 1;
|
m_nameIndex[key] = m_icons.size() - 1;
|
||||||
}
|
}
|
||||||
endInsertRows();
|
endInsertRows();
|
||||||
return true;
|
return true;
|
||||||
@ -389,33 +430,32 @@ void IconList::saveIcon(const QString& key, const QString& path, const char* for
|
|||||||
|
|
||||||
void IconList::reindex()
|
void IconList::reindex()
|
||||||
{
|
{
|
||||||
name_index.clear();
|
m_nameIndex.clear();
|
||||||
int i = 0;
|
for (int i = 0; i < m_icons.size(); i++) {
|
||||||
for (auto& iter : icons) {
|
m_nameIndex[m_icons[i].m_key] = i;
|
||||||
name_index[iter.m_key] = i;
|
emit iconUpdated(m_icons[i].m_key); // prevents incorrect indices with proxy model
|
||||||
i++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QIcon IconList::getIcon(const QString& key) const
|
QIcon IconList::getIcon(const QString& key) const
|
||||||
{
|
{
|
||||||
int icon_index = getIconIndex(key);
|
int iconIndex = getIconIndex(key);
|
||||||
|
|
||||||
if (icon_index != -1)
|
if (iconIndex != -1)
|
||||||
return icons[icon_index].icon();
|
return m_icons[iconIndex].icon();
|
||||||
|
|
||||||
// Fallback for icons that don't exist.
|
// Fallback for icons that don't exist.b
|
||||||
icon_index = getIconIndex("grass");
|
iconIndex = getIconIndex("grass");
|
||||||
|
|
||||||
if (icon_index != -1)
|
if (iconIndex != -1)
|
||||||
return icons[icon_index].icon();
|
return m_icons[iconIndex].icon();
|
||||||
return QIcon();
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
int IconList::getIconIndex(const QString& key) const
|
int IconList::getIconIndex(const QString& key) const
|
||||||
{
|
{
|
||||||
auto iter = name_index.find(key == "default" ? "grass" : key);
|
auto iter = m_nameIndex.find(key == "default" ? "grass" : key);
|
||||||
if (iter != name_index.end())
|
if (iter != m_nameIndex.end())
|
||||||
return *iter;
|
return *iter;
|
||||||
|
|
||||||
return -1;
|
return -1;
|
||||||
@ -425,3 +465,15 @@ QString IconList::getDirectory() const
|
|||||||
{
|
{
|
||||||
return m_dir.absolutePath();
|
return m_dir.absolutePath();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the directory of the icon with the given key or the default directory if it's a builtin icon.
|
||||||
|
QString IconList::iconDirectory(const QString& key) const
|
||||||
|
{
|
||||||
|
for (const auto& mmcIcon : m_icons) {
|
||||||
|
if (mmcIcon.m_key == key && mmcIcon.has(IconType::FileBased)) {
|
||||||
|
QFileInfo iconFile(mmcIcon.getFilePath());
|
||||||
|
return iconFile.dir().path();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return getDirectory();
|
||||||
|
}
|
||||||
|
@ -51,7 +51,7 @@ class QFileSystemWatcher;
|
|||||||
class IconList : public QAbstractListModel {
|
class IconList : public QAbstractListModel {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
explicit IconList(const QStringList& builtinPaths, QString path, QObject* parent = 0);
|
explicit IconList(const QStringList& builtinPaths, const QString& path, QObject* parent = 0);
|
||||||
virtual ~IconList() {};
|
virtual ~IconList() {};
|
||||||
|
|
||||||
QIcon getIcon(const QString& key) const;
|
QIcon getIcon(const QString& key) const;
|
||||||
@ -72,6 +72,7 @@ class IconList : public QAbstractListModel {
|
|||||||
bool deleteIcon(const QString& key);
|
bool deleteIcon(const QString& key);
|
||||||
bool trashIcon(const QString& key);
|
bool trashIcon(const QString& key);
|
||||||
bool iconFileExists(const QString& key) const;
|
bool iconFileExists(const QString& key) const;
|
||||||
|
QString iconDirectory(const QString& key) const;
|
||||||
|
|
||||||
void installIcons(const QStringList& iconFiles);
|
void installIcons(const QStringList& iconFiles);
|
||||||
void installIcon(const QString& file, const QString& name);
|
void installIcon(const QString& file, const QString& name);
|
||||||
@ -91,18 +92,20 @@ class IconList : public QAbstractListModel {
|
|||||||
IconList& operator=(const IconList&) = delete;
|
IconList& operator=(const IconList&) = delete;
|
||||||
void reindex();
|
void reindex();
|
||||||
void sortIconList();
|
void sortIconList();
|
||||||
|
bool addPathRecursively(const QString& path);
|
||||||
|
QStringList getIconFilePaths() const;
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void directoryChanged(const QString& path);
|
void directoryChanged(const QString& path);
|
||||||
|
|
||||||
protected slots:
|
protected slots:
|
||||||
void fileChanged(const QString& path);
|
void fileChanged(const QString& path);
|
||||||
void SettingChanged(const Setting& setting, QVariant value);
|
void SettingChanged(const Setting& setting, const QVariant& value);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
shared_qobject_ptr<QFileSystemWatcher> m_watcher;
|
shared_qobject_ptr<QFileSystemWatcher> m_watcher;
|
||||||
bool is_watching;
|
bool m_isWatching;
|
||||||
QMap<QString, int> name_index;
|
QMap<QString, int> m_nameIndex;
|
||||||
QVector<MMCIcon> icons;
|
QVector<MMCIcon> m_icons;
|
||||||
QDir m_dir;
|
QDir m_dir;
|
||||||
};
|
};
|
||||||
|
@ -86,11 +86,10 @@ void ManifestDownloadTask::downloadJava(const QJsonDocument& doc)
|
|||||||
if (type == "directory") {
|
if (type == "directory") {
|
||||||
FS::ensureFolderPathExists(file);
|
FS::ensureFolderPathExists(file);
|
||||||
} else if (type == "link") {
|
} else if (type == "link") {
|
||||||
// this is linux only !
|
// this is *nix only !
|
||||||
auto path = Json::ensureString(meta, "target");
|
auto path = Json::ensureString(meta, "target");
|
||||||
if (!path.isEmpty()) {
|
if (!path.isEmpty()) {
|
||||||
auto target = FS::PathCombine(file, "../" + path);
|
QFile::link(path, file);
|
||||||
QFile(target).link(file);
|
|
||||||
}
|
}
|
||||||
} else if (type == "file") {
|
} else if (type == "file") {
|
||||||
// TODO download compressed version if it exists ?
|
// TODO download compressed version if it exists ?
|
||||||
|
@ -254,20 +254,60 @@ void LaunchTask::emitFailed(QString reason)
|
|||||||
Task::emitFailed(reason);
|
Task::emitFailed(reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
void LaunchTask::substituteVariables(QStringList& args) const
|
QString expandVariables(const QString& input, QProcessEnvironment dict)
|
||||||
{
|
{
|
||||||
auto env = m_instance->createEnvironment();
|
QString result = input;
|
||||||
|
|
||||||
for (auto key : env.keys()) {
|
enum { base, maybeBrace, variable, brace } state = base;
|
||||||
args.replaceInStrings("$" + key, env.value(key));
|
int startIdx = -1;
|
||||||
|
for (int i = 0; i < result.length();) {
|
||||||
|
QChar c = result.at(i++);
|
||||||
|
switch (state) {
|
||||||
|
case base:
|
||||||
|
if (c == '$')
|
||||||
|
state = maybeBrace;
|
||||||
|
break;
|
||||||
|
case maybeBrace:
|
||||||
|
if (c == '{') {
|
||||||
|
state = brace;
|
||||||
|
startIdx = i;
|
||||||
|
} else if (c.isLetterOrNumber() || c == '_') {
|
||||||
|
state = variable;
|
||||||
|
startIdx = i - 1;
|
||||||
|
} else {
|
||||||
|
state = base;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case brace:
|
||||||
|
if (c == '}') {
|
||||||
|
const auto res = dict.value(result.mid(startIdx, i - 1 - startIdx), "");
|
||||||
|
if (!res.isEmpty()) {
|
||||||
|
result.replace(startIdx - 2, i - startIdx + 2, res);
|
||||||
|
i = startIdx - 2 + res.length();
|
||||||
|
}
|
||||||
|
state = base;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case variable:
|
||||||
|
if (!c.isLetterOrNumber() && c != '_') {
|
||||||
|
const auto res = dict.value(result.mid(startIdx, i - startIdx - 1), "");
|
||||||
|
if (!res.isEmpty()) {
|
||||||
|
result.replace(startIdx - 1, i - startIdx, res);
|
||||||
|
i = startIdx - 1 + res.length();
|
||||||
|
}
|
||||||
|
state = base;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if (state == variable) {
|
||||||
|
if (const auto res = dict.value(result.mid(startIdx), ""); !res.isEmpty())
|
||||||
|
result.replace(startIdx - 1, result.length() - startIdx + 1, res);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
void LaunchTask::substituteVariables(QString& cmd) const
|
QString LaunchTask::substituteVariables(QString& cmd, bool isLaunch) const
|
||||||
{
|
{
|
||||||
auto env = m_instance->createEnvironment();
|
return expandVariables(cmd, isLaunch ? m_instance->createLaunchEnvironment() : m_instance->createEnvironment());
|
||||||
|
|
||||||
for (auto key : env.keys()) {
|
|
||||||
cmd.replace("$" + key, env.value(key));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -87,8 +87,7 @@ class LaunchTask : public Task {
|
|||||||
shared_qobject_ptr<LogModel> getLogModel();
|
shared_qobject_ptr<LogModel> getLogModel();
|
||||||
|
|
||||||
public:
|
public:
|
||||||
void substituteVariables(QStringList& args) const;
|
QString substituteVariables(QString& cmd, bool isLaunch = false) const;
|
||||||
void substituteVariables(QString& cmd) const;
|
|
||||||
QString censorPrivateInfo(QString in);
|
QString censorPrivateInfo(QString in);
|
||||||
|
|
||||||
protected: /* methods */
|
protected: /* methods */
|
||||||
|
@ -47,19 +47,15 @@ PostLaunchCommand::PostLaunchCommand(LaunchTask* parent) : LaunchStep(parent)
|
|||||||
|
|
||||||
void PostLaunchCommand::executeTask()
|
void PostLaunchCommand::executeTask()
|
||||||
{
|
{
|
||||||
// FIXME: where to put this?
|
auto cmd = m_parent->substituteVariables(m_command);
|
||||||
|
emit logLine(tr("Running Post-Launch command: %1").arg(cmd), MessageLevel::Launcher);
|
||||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
|
||||||
auto args = QProcess::splitCommand(m_command);
|
auto args = QProcess::splitCommand(cmd);
|
||||||
m_parent->substituteVariables(args);
|
|
||||||
|
|
||||||
emit logLine(tr("Running Post-Launch command: %1").arg(args.join(' ')), MessageLevel::Launcher);
|
|
||||||
const QString program = args.takeFirst();
|
const QString program = args.takeFirst();
|
||||||
m_process.start(program, args);
|
m_process.start(program, args);
|
||||||
#else
|
#else
|
||||||
m_parent->substituteVariables(m_command);
|
m_process.start(cmd);
|
||||||
|
|
||||||
emit logLine(tr("Running Post-Launch command: %1").arg(m_command), MessageLevel::Launcher);
|
|
||||||
m_process.start(m_command);
|
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,19 +47,14 @@ PreLaunchCommand::PreLaunchCommand(LaunchTask* parent) : LaunchStep(parent)
|
|||||||
|
|
||||||
void PreLaunchCommand::executeTask()
|
void PreLaunchCommand::executeTask()
|
||||||
{
|
{
|
||||||
// FIXME: where to put this?
|
auto cmd = m_parent->substituteVariables(m_command);
|
||||||
|
emit logLine(tr("Running Pre-Launch command: %1").arg(cmd), MessageLevel::Launcher);
|
||||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
|
||||||
auto args = QProcess::splitCommand(m_command);
|
auto args = QProcess::splitCommand(cmd);
|
||||||
m_parent->substituteVariables(args);
|
|
||||||
|
|
||||||
emit logLine(tr("Running Pre-Launch command: %1").arg(args.join(' ')), MessageLevel::Launcher);
|
|
||||||
const QString program = args.takeFirst();
|
const QString program = args.takeFirst();
|
||||||
m_process.start(program, args);
|
m_process.start(program, args);
|
||||||
#else
|
#else
|
||||||
m_parent->substituteVariables(m_command);
|
m_process.start(cmd);
|
||||||
|
|
||||||
emit logLine(tr("Running Pre-Launch command: %1").arg(m_command), MessageLevel::Launcher);
|
|
||||||
m_process.start(m_command);
|
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -594,6 +594,13 @@ QMap<QString, QString> MinecraftInstance::getVariables()
|
|||||||
out.insert("INST_JAVA", settings()->get("JavaPath").toString());
|
out.insert("INST_JAVA", settings()->get("JavaPath").toString());
|
||||||
out.insert("INST_JAVA_ARGS", javaArguments().join(' '));
|
out.insert("INST_JAVA_ARGS", javaArguments().join(' '));
|
||||||
out.insert("NO_COLOR", "1");
|
out.insert("NO_COLOR", "1");
|
||||||
|
#ifdef Q_OS_MACOS
|
||||||
|
// get library for Steam overlay support
|
||||||
|
QString steamDyldInsertLibraries = qEnvironmentVariable("STEAM_DYLD_INSERT_LIBRARIES");
|
||||||
|
if (!steamDyldInsertLibraries.isEmpty()) {
|
||||||
|
out.insert("DYLD_INSERT_LIBRARIES", steamDyldInsertLibraries);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1151,13 +1158,6 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt
|
|||||||
process->appendStep(step);
|
process->appendStep(step);
|
||||||
}
|
}
|
||||||
|
|
||||||
// run pre-launch command if that's needed
|
|
||||||
if (getPreLaunchCommand().size()) {
|
|
||||||
auto step = makeShared<PreLaunchCommand>(pptr);
|
|
||||||
step->setWorkingDirectory(gameRoot());
|
|
||||||
process->appendStep(step);
|
|
||||||
}
|
|
||||||
|
|
||||||
// load meta
|
// load meta
|
||||||
{
|
{
|
||||||
auto mode = session->status != AuthSession::PlayableOffline ? Net::Mode::Online : Net::Mode::Offline;
|
auto mode = session->status != AuthSession::PlayableOffline ? Net::Mode::Online : Net::Mode::Offline;
|
||||||
@ -1170,6 +1170,13 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt
|
|||||||
process->appendStep(makeShared<CheckJava>(pptr));
|
process->appendStep(makeShared<CheckJava>(pptr));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// run pre-launch command if that's needed
|
||||||
|
if (getPreLaunchCommand().size()) {
|
||||||
|
auto step = makeShared<PreLaunchCommand>(pptr);
|
||||||
|
step->setWorkingDirectory(gameRoot());
|
||||||
|
process->appendStep(step);
|
||||||
|
}
|
||||||
|
|
||||||
// if we aren't in offline mode,.
|
// if we aren't in offline mode,.
|
||||||
if (session->status != AuthSession::PlayableOffline) {
|
if (session->status != AuthSession::PlayableOffline) {
|
||||||
if (!session->demo) {
|
if (!session->demo) {
|
||||||
|
@ -8,7 +8,10 @@ void MinecraftLoadAndCheck::executeTask()
|
|||||||
{
|
{
|
||||||
// add offline metadata load task
|
// add offline metadata load task
|
||||||
auto components = m_inst->getPackProfile();
|
auto components = m_inst->getPackProfile();
|
||||||
components->reload(m_netmode);
|
if (auto result = components->reload(m_netmode); !result) {
|
||||||
|
emitFailed(result.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
m_task = components->getCurrentTask();
|
m_task = components->getCurrentTask();
|
||||||
|
|
||||||
if (!m_task) {
|
if (!m_task) {
|
||||||
|
@ -173,29 +173,32 @@ static bool savePackProfile(const QString& filename, const ComponentContainer& c
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Read the given file into component containers
|
// Read the given file into component containers
|
||||||
static bool loadPackProfile(PackProfile* parent,
|
static PackProfile::Result loadPackProfile(PackProfile* parent,
|
||||||
const QString& filename,
|
const QString& filename,
|
||||||
const QString& componentJsonPattern,
|
const QString& componentJsonPattern,
|
||||||
ComponentContainer& container)
|
ComponentContainer& container)
|
||||||
{
|
{
|
||||||
QFile componentsFile(filename);
|
QFile componentsFile(filename);
|
||||||
if (!componentsFile.exists()) {
|
if (!componentsFile.exists()) {
|
||||||
qCWarning(instanceProfileC) << "Components file" << filename << "doesn't exist. This should never happen.";
|
auto message = QObject::tr("Components file %1 doesn't exist. This should never happen.").arg(filename);
|
||||||
return false;
|
qCWarning(instanceProfileC) << message;
|
||||||
|
return PackProfile::Result::Error(message);
|
||||||
}
|
}
|
||||||
if (!componentsFile.open(QFile::ReadOnly)) {
|
if (!componentsFile.open(QFile::ReadOnly)) {
|
||||||
qCCritical(instanceProfileC) << "Couldn't open" << componentsFile.fileName() << " for reading:" << componentsFile.errorString();
|
auto message = QObject::tr("Couldn't open %1 for reading: %2").arg(componentsFile.fileName(), componentsFile.errorString());
|
||||||
|
qCCritical(instanceProfileC) << message;
|
||||||
qCWarning(instanceProfileC) << "Ignoring overridden order";
|
qCWarning(instanceProfileC) << "Ignoring overridden order";
|
||||||
return false;
|
return PackProfile::Result::Error(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
// and it's valid JSON
|
// and it's valid JSON
|
||||||
QJsonParseError error;
|
QJsonParseError error;
|
||||||
QJsonDocument doc = QJsonDocument::fromJson(componentsFile.readAll(), &error);
|
QJsonDocument doc = QJsonDocument::fromJson(componentsFile.readAll(), &error);
|
||||||
if (error.error != QJsonParseError::NoError) {
|
if (error.error != QJsonParseError::NoError) {
|
||||||
qCCritical(instanceProfileC) << "Couldn't parse" << componentsFile.fileName() << ":" << error.errorString();
|
auto message = QObject::tr("Couldn't parse %1 as json: %2").arg(componentsFile.fileName(), error.errorString());
|
||||||
|
qCCritical(instanceProfileC) << message;
|
||||||
qCWarning(instanceProfileC) << "Ignoring overridden order";
|
qCWarning(instanceProfileC) << "Ignoring overridden order";
|
||||||
return false;
|
return PackProfile::Result::Error(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
// and then read it and process it if all above is true.
|
// and then read it and process it if all above is true.
|
||||||
@ -212,11 +215,13 @@ static bool loadPackProfile(PackProfile* parent,
|
|||||||
container.append(componentFromJsonV1(parent, componentJsonPattern, comp_obj));
|
container.append(componentFromJsonV1(parent, componentJsonPattern, comp_obj));
|
||||||
}
|
}
|
||||||
} catch ([[maybe_unused]] const JSONValidationError& err) {
|
} catch ([[maybe_unused]] const JSONValidationError& err) {
|
||||||
qCCritical(instanceProfileC) << "Couldn't parse" << componentsFile.fileName() << ": bad file format";
|
auto message = QObject::tr("Couldn't parse %1 : bad file format").arg(componentsFile.fileName());
|
||||||
|
qCCritical(instanceProfileC) << message;
|
||||||
|
qCWarning(instanceProfileC) << "error:" << err.what();
|
||||||
container.clear();
|
container.clear();
|
||||||
return false;
|
return PackProfile::Result::Error(message);
|
||||||
}
|
}
|
||||||
return true;
|
return PackProfile::Result::Success();
|
||||||
}
|
}
|
||||||
|
|
||||||
// END: component file format
|
// END: component file format
|
||||||
@ -283,44 +288,43 @@ void PackProfile::save_internal()
|
|||||||
d->dirty = false;
|
d->dirty = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool PackProfile::load()
|
PackProfile::Result PackProfile::load()
|
||||||
{
|
{
|
||||||
auto filename = componentsFilePath();
|
auto filename = componentsFilePath();
|
||||||
|
|
||||||
// load the new component list and swap it with the current one...
|
// load the new component list and swap it with the current one...
|
||||||
ComponentContainer newComponents;
|
ComponentContainer newComponents;
|
||||||
if (!loadPackProfile(this, filename, patchesPattern(), newComponents)) {
|
if (auto result = loadPackProfile(this, filename, patchesPattern(), newComponents); !result) {
|
||||||
qCritical() << d->m_instance->name() << "|" << "Failed to load the component config";
|
qCritical() << d->m_instance->name() << "|" << "Failed to load the component config";
|
||||||
return false;
|
return result;
|
||||||
} else {
|
|
||||||
// FIXME: actually use fine-grained updates, not this...
|
|
||||||
beginResetModel();
|
|
||||||
// disconnect all the old components
|
|
||||||
for (auto component : d->components) {
|
|
||||||
disconnect(component.get(), &Component::dataChanged, this, &PackProfile::componentDataChanged);
|
|
||||||
}
|
|
||||||
d->components.clear();
|
|
||||||
d->componentIndex.clear();
|
|
||||||
for (auto component : newComponents) {
|
|
||||||
if (d->componentIndex.contains(component->m_uid)) {
|
|
||||||
qWarning() << d->m_instance->name() << "|" << "Ignoring duplicate component entry" << component->m_uid;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
connect(component.get(), &Component::dataChanged, this, &PackProfile::componentDataChanged);
|
|
||||||
d->components.append(component);
|
|
||||||
d->componentIndex[component->m_uid] = component;
|
|
||||||
}
|
|
||||||
endResetModel();
|
|
||||||
d->loaded = true;
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
// FIXME: actually use fine-grained updates, not this...
|
||||||
|
beginResetModel();
|
||||||
|
// disconnect all the old components
|
||||||
|
for (auto component : d->components) {
|
||||||
|
disconnect(component.get(), &Component::dataChanged, this, &PackProfile::componentDataChanged);
|
||||||
|
}
|
||||||
|
d->components.clear();
|
||||||
|
d->componentIndex.clear();
|
||||||
|
for (auto component : newComponents) {
|
||||||
|
if (d->componentIndex.contains(component->m_uid)) {
|
||||||
|
qWarning() << d->m_instance->name() << "|" << "Ignoring duplicate component entry" << component->m_uid;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
connect(component.get(), &Component::dataChanged, this, &PackProfile::componentDataChanged);
|
||||||
|
d->components.append(component);
|
||||||
|
d->componentIndex[component->m_uid] = component;
|
||||||
|
}
|
||||||
|
endResetModel();
|
||||||
|
d->loaded = true;
|
||||||
|
return Result::Success();
|
||||||
}
|
}
|
||||||
|
|
||||||
void PackProfile::reload(Net::Mode netmode)
|
PackProfile::Result PackProfile::reload(Net::Mode netmode)
|
||||||
{
|
{
|
||||||
// Do not reload when the update/resolve task is running. It is in control.
|
// Do not reload when the update/resolve task is running. It is in control.
|
||||||
if (d->m_updateTask) {
|
if (d->m_updateTask) {
|
||||||
return;
|
return Result::Success();
|
||||||
}
|
}
|
||||||
|
|
||||||
// flush any scheduled saves to not lose state
|
// flush any scheduled saves to not lose state
|
||||||
@ -329,9 +333,11 @@ void PackProfile::reload(Net::Mode netmode)
|
|||||||
// FIXME: differentiate when a reapply is required by propagating state from components
|
// FIXME: differentiate when a reapply is required by propagating state from components
|
||||||
invalidateLaunchProfile();
|
invalidateLaunchProfile();
|
||||||
|
|
||||||
if (load()) {
|
if (auto result = load(); !result) {
|
||||||
resolve(netmode);
|
return result;
|
||||||
}
|
}
|
||||||
|
resolve(netmode);
|
||||||
|
return Result::Success();
|
||||||
}
|
}
|
||||||
|
|
||||||
Task::Ptr PackProfile::getCurrentTask()
|
Task::Ptr PackProfile::getCurrentTask()
|
||||||
|
@ -62,6 +62,19 @@ class PackProfile : public QAbstractListModel {
|
|||||||
public:
|
public:
|
||||||
enum Columns { NameColumn = 0, VersionColumn, NUM_COLUMNS };
|
enum Columns { NameColumn = 0, VersionColumn, NUM_COLUMNS };
|
||||||
|
|
||||||
|
struct Result {
|
||||||
|
bool success;
|
||||||
|
QString error;
|
||||||
|
|
||||||
|
// Implicit conversion to bool
|
||||||
|
operator bool() const { return success; }
|
||||||
|
|
||||||
|
// Factory methods for convenience
|
||||||
|
static Result Success() { return { true, "" }; }
|
||||||
|
|
||||||
|
static Result Error(const QString& errorMessage) { return { false, errorMessage }; }
|
||||||
|
};
|
||||||
|
|
||||||
explicit PackProfile(MinecraftInstance* instance);
|
explicit PackProfile(MinecraftInstance* instance);
|
||||||
virtual ~PackProfile();
|
virtual ~PackProfile();
|
||||||
|
|
||||||
@ -102,7 +115,7 @@ class PackProfile : public QAbstractListModel {
|
|||||||
bool revertToBase(int index);
|
bool revertToBase(int index);
|
||||||
|
|
||||||
/// reload the list, reload all components, resolve dependencies
|
/// reload the list, reload all components, resolve dependencies
|
||||||
void reload(Net::Mode netmode);
|
Result reload(Net::Mode netmode);
|
||||||
|
|
||||||
// reload all components, resolve dependencies
|
// reload all components, resolve dependencies
|
||||||
void resolve(Net::Mode netmode);
|
void resolve(Net::Mode netmode);
|
||||||
@ -169,7 +182,7 @@ class PackProfile : public QAbstractListModel {
|
|||||||
void disableInteraction(bool disable);
|
void disableInteraction(bool disable);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool load();
|
Result load();
|
||||||
bool installJarMods_internal(QStringList filepaths);
|
bool installJarMods_internal(QStringList filepaths);
|
||||||
bool installCustomJar_internal(QString filepath);
|
bool installCustomJar_internal(QString filepath);
|
||||||
bool installAgents_internal(QStringList filepaths);
|
bool installAgents_internal(QStringList filepaths);
|
||||||
|
@ -131,6 +131,7 @@ void LauncherPartLaunch::executeTask()
|
|||||||
|
|
||||||
QString wrapperCommandStr = instance->getWrapperCommand().trimmed();
|
QString wrapperCommandStr = instance->getWrapperCommand().trimmed();
|
||||||
if (!wrapperCommandStr.isEmpty()) {
|
if (!wrapperCommandStr.isEmpty()) {
|
||||||
|
wrapperCommandStr = m_parent->substituteVariables(wrapperCommandStr);
|
||||||
auto wrapperArgs = Commandline::splitArgs(wrapperCommandStr);
|
auto wrapperArgs = Commandline::splitArgs(wrapperCommandStr);
|
||||||
auto wrapperCommand = wrapperArgs.takeFirst();
|
auto wrapperCommand = wrapperArgs.takeFirst();
|
||||||
auto realWrapperCommand = QStandardPaths::findExecutable(wrapperCommand);
|
auto realWrapperCommand = QStandardPaths::findExecutable(wrapperCommand);
|
||||||
|
@ -48,6 +48,7 @@
|
|||||||
#include "Version.h"
|
#include "Version.h"
|
||||||
#include "minecraft/mod/ModDetails.h"
|
#include "minecraft/mod/ModDetails.h"
|
||||||
#include "minecraft/mod/tasks/LocalModParseTask.h"
|
#include "minecraft/mod/tasks/LocalModParseTask.h"
|
||||||
|
#include "modplatform/ModIndex.h"
|
||||||
|
|
||||||
Mod::Mod(const QFileInfo& file) : Resource(file), m_local_details()
|
Mod::Mod(const QFileInfo& file) : Resource(file), m_local_details()
|
||||||
{
|
{
|
||||||
@ -157,11 +158,8 @@ auto Mod::loaders() const -> QString
|
|||||||
if (metadata()) {
|
if (metadata()) {
|
||||||
QStringList loaders;
|
QStringList loaders;
|
||||||
auto modLoaders = metadata()->loaders;
|
auto modLoaders = metadata()->loaders;
|
||||||
for (auto loader : { ModPlatform::NeoForge, ModPlatform::Forge, ModPlatform::Cauldron, ModPlatform::LiteLoader, ModPlatform::Fabric,
|
for (auto loader : ModPlatform::modLoaderTypesToList(modLoaders)) {
|
||||||
ModPlatform::Quilt }) {
|
loaders << getModLoaderAsString(loader);
|
||||||
if (modLoaders & loader) {
|
|
||||||
loaders << getModLoaderAsString(loader);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return loaders.join(", ");
|
return loaders.join(", ");
|
||||||
}
|
}
|
||||||
|
@ -19,27 +19,28 @@ static ModrinthAPI modrinth_api;
|
|||||||
static FlameAPI flame_api;
|
static FlameAPI flame_api;
|
||||||
|
|
||||||
EnsureMetadataTask::EnsureMetadataTask(Resource* resource, QDir dir, ModPlatform::ResourceProvider prov)
|
EnsureMetadataTask::EnsureMetadataTask(Resource* resource, QDir dir, ModPlatform::ResourceProvider prov)
|
||||||
: Task(), m_index_dir(dir), m_provider(prov), m_hashing_task(nullptr), m_current_task(nullptr)
|
: Task(), m_index_dir(dir), m_provider(prov), m_hashingTask(nullptr), m_current_task(nullptr)
|
||||||
{
|
{
|
||||||
auto hash_task = createNewHash(resource);
|
auto hashTask = createNewHash(resource);
|
||||||
if (!hash_task)
|
if (!hashTask)
|
||||||
return;
|
return;
|
||||||
connect(hash_task.get(), &Hashing::Hasher::resultsReady, [this, resource](QString hash) { m_resources.insert(hash, resource); });
|
connect(hashTask.get(), &Hashing::Hasher::resultsReady, [this, resource](QString hash) { m_resources.insert(hash, resource); });
|
||||||
connect(hash_task.get(), &Task::failed, [this, resource] { emitFail(resource, "", RemoveFromList::No); });
|
connect(hashTask.get(), &Task::failed, [this, resource] { emitFail(resource, "", RemoveFromList::No); });
|
||||||
hash_task->start();
|
m_hashingTask = hashTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
EnsureMetadataTask::EnsureMetadataTask(QList<Resource*>& resources, QDir dir, ModPlatform::ResourceProvider prov)
|
EnsureMetadataTask::EnsureMetadataTask(QList<Resource*>& resources, QDir dir, ModPlatform::ResourceProvider prov)
|
||||||
: Task(), m_index_dir(dir), m_provider(prov), m_current_task(nullptr)
|
: Task(), m_index_dir(dir), m_provider(prov), m_current_task(nullptr)
|
||||||
{
|
{
|
||||||
m_hashing_task.reset(new ConcurrentTask("MakeHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt()));
|
auto hashTask = makeShared<ConcurrentTask>("MakeHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt());
|
||||||
|
m_hashingTask = hashTask;
|
||||||
for (auto* resource : resources) {
|
for (auto* resource : resources) {
|
||||||
auto hash_task = createNewHash(resource);
|
auto hash_task = createNewHash(resource);
|
||||||
if (!hash_task)
|
if (!hash_task)
|
||||||
continue;
|
continue;
|
||||||
connect(hash_task.get(), &Hashing::Hasher::resultsReady, [this, resource](QString hash) { m_resources.insert(hash, resource); });
|
connect(hash_task.get(), &Hashing::Hasher::resultsReady, [this, resource](QString hash) { m_resources.insert(hash, resource); });
|
||||||
connect(hash_task.get(), &Task::failed, [this, resource] { emitFail(resource, "", RemoveFromList::No); });
|
connect(hash_task.get(), &Task::failed, [this, resource] { emitFail(resource, "", RemoveFromList::No); });
|
||||||
m_hashing_task->addTask(hash_task);
|
hashTask->addTask(hash_task);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ class EnsureMetadataTask : public Task {
|
|||||||
|
|
||||||
~EnsureMetadataTask() = default;
|
~EnsureMetadataTask() = default;
|
||||||
|
|
||||||
Task::Ptr getHashingTask() { return m_hashing_task; }
|
Task::Ptr getHashingTask() { return m_hashingTask; }
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
bool abort() override;
|
bool abort() override;
|
||||||
@ -59,6 +59,6 @@ class EnsureMetadataTask : public Task {
|
|||||||
ModPlatform::ResourceProvider m_provider;
|
ModPlatform::ResourceProvider m_provider;
|
||||||
|
|
||||||
QHash<QString, ModPlatform::IndexedVersion> m_temp_versions;
|
QHash<QString, ModPlatform::IndexedVersion> m_temp_versions;
|
||||||
ConcurrentTask::Ptr m_hashing_task;
|
Task::Ptr m_hashingTask;
|
||||||
Task::Ptr m_current_task;
|
Task::Ptr m_current_task;
|
||||||
};
|
};
|
||||||
|
@ -31,6 +31,19 @@ static const QMap<QString, IndexedVersionType::VersionType> s_indexed_version_ty
|
|||||||
{ "alpha", IndexedVersionType::VersionType::Alpha }
|
{ "alpha", IndexedVersionType::VersionType::Alpha }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static const QList<ModLoaderType> loaderList = { NeoForge, Forge, Cauldron, LiteLoader, Quilt, Fabric };
|
||||||
|
|
||||||
|
QList<ModLoaderType> modLoaderTypesToList(ModLoaderTypes flags)
|
||||||
|
{
|
||||||
|
QList<ModLoaderType> flagList;
|
||||||
|
for (auto flag : loaderList) {
|
||||||
|
if (flags.testFlag(flag)) {
|
||||||
|
flagList.append(flag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return flagList;
|
||||||
|
}
|
||||||
|
|
||||||
IndexedVersionType::IndexedVersionType(const QString& type) : IndexedVersionType(enumFromString(type)) {}
|
IndexedVersionType::IndexedVersionType(const QString& type) : IndexedVersionType(enumFromString(type)) {}
|
||||||
|
|
||||||
IndexedVersionType::IndexedVersionType(const IndexedVersionType::VersionType& type)
|
IndexedVersionType::IndexedVersionType(const IndexedVersionType::VersionType& type)
|
||||||
|
@ -32,6 +32,7 @@ namespace ModPlatform {
|
|||||||
|
|
||||||
enum ModLoaderType { NeoForge = 1 << 0, Forge = 1 << 1, Cauldron = 1 << 2, LiteLoader = 1 << 3, Fabric = 1 << 4, Quilt = 1 << 5 };
|
enum ModLoaderType { NeoForge = 1 << 0, Forge = 1 << 1, Cauldron = 1 << 2, LiteLoader = 1 << 3, Fabric = 1 << 4, Quilt = 1 << 5 };
|
||||||
Q_DECLARE_FLAGS(ModLoaderTypes, ModLoaderType)
|
Q_DECLARE_FLAGS(ModLoaderTypes, ModLoaderType)
|
||||||
|
QList<ModLoaderType> modLoaderTypesToList(ModLoaderTypes flags);
|
||||||
|
|
||||||
enum class ResourceProvider { MODRINTH, FLAME };
|
enum class ResourceProvider { MODRINTH, FLAME };
|
||||||
|
|
||||||
|
@ -87,6 +87,30 @@ void Flame::FileResolvingTask::executeTask()
|
|||||||
m_task->start();
|
m_task->start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PackedResourceType getResourceType(int classId)
|
||||||
|
{
|
||||||
|
switch (classId) {
|
||||||
|
case 17: // Worlds
|
||||||
|
return PackedResourceType::WorldSave;
|
||||||
|
case 6: // Mods
|
||||||
|
return PackedResourceType::Mod;
|
||||||
|
case 12: // Resource Packs
|
||||||
|
// return PackedResourceType::ResourcePack; // not really a resourcepack
|
||||||
|
/* fallthrough */
|
||||||
|
case 4546: // Customization
|
||||||
|
// return PackedResourceType::ShaderPack; // not really a shaderPack
|
||||||
|
/* fallthrough */
|
||||||
|
case 4471: // Modpacks
|
||||||
|
/* fallthrough */
|
||||||
|
case 5: // Bukkit Plugins
|
||||||
|
/* fallthrough */
|
||||||
|
case 4559: // Addons
|
||||||
|
/* fallthrough */
|
||||||
|
default:
|
||||||
|
return PackedResourceType::UNKNOWN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void Flame::FileResolvingTask::netJobFinished()
|
void Flame::FileResolvingTask::netJobFinished()
|
||||||
{
|
{
|
||||||
setProgress(1, 3);
|
setProgress(1, 3);
|
||||||
@ -144,7 +168,7 @@ void Flame::FileResolvingTask::netJobFinished()
|
|||||||
<< " reason: " << parse_error.errorString();
|
<< " reason: " << parse_error.errorString();
|
||||||
qWarning() << *m_result;
|
qWarning() << *m_result;
|
||||||
|
|
||||||
failed(parse_error.errorString());
|
getFlameProjects();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -232,6 +256,10 @@ void Flame::FileResolvingTask::getFlameProjects()
|
|||||||
|
|
||||||
setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(file->version.fileName));
|
setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(file->version.fileName));
|
||||||
FlameMod::loadIndexedPack(file->pack, entry_obj);
|
FlameMod::loadIndexedPack(file->pack, entry_obj);
|
||||||
|
file->resourceType = getResourceType(Json::requireInteger(entry_obj, "classId", "modClassId"));
|
||||||
|
if (file->resourceType == PackedResourceType::WorldSave) {
|
||||||
|
file->targetFolder = "saves";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (Json::JsonException& e) {
|
} catch (Json::JsonException& e) {
|
||||||
qDebug() << e.cause();
|
qDebug() << e.cause();
|
||||||
|
@ -270,21 +270,44 @@ std::optional<ModPlatform::IndexedVersion> FlameAPI::getLatestVersion(QList<ModP
|
|||||||
QList<ModPlatform::ModLoaderType> instanceLoaders,
|
QList<ModPlatform::ModLoaderType> instanceLoaders,
|
||||||
ModPlatform::ModLoaderTypes modLoaders)
|
ModPlatform::ModLoaderTypes modLoaders)
|
||||||
{
|
{
|
||||||
// edge case: mod has installed for forge but the instance is fabric => fabric version will be prioritizated on update
|
static const auto noLoader = ModPlatform::ModLoaderType(0);
|
||||||
auto bestVersion = [&versions](ModPlatform::ModLoaderTypes loader) {
|
QHash<ModPlatform::ModLoaderType, ModPlatform::IndexedVersion> bestMatch;
|
||||||
std::optional<ModPlatform::IndexedVersion> ver;
|
auto checkVersion = [&bestMatch](const ModPlatform::IndexedVersion& version, const ModPlatform::ModLoaderType& loader) {
|
||||||
for (auto file_tmp : versions) {
|
if (bestMatch.contains(loader)) {
|
||||||
if (file_tmp.loaders & loader && (!ver.has_value() || file_tmp.date > ver->date)) {
|
auto best = bestMatch.value(loader);
|
||||||
ver = file_tmp;
|
if (version.date > best.date) {
|
||||||
|
bestMatch[loader] = version;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bestMatch[loader] = version;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for (auto file_tmp : versions) {
|
||||||
|
auto loaders = ModPlatform::modLoaderTypesToList(file_tmp.loaders);
|
||||||
|
if (loaders.isEmpty()) {
|
||||||
|
checkVersion(file_tmp, noLoader);
|
||||||
|
} else {
|
||||||
|
for (auto loader : loaders) {
|
||||||
|
checkVersion(file_tmp, loader);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ver;
|
}
|
||||||
};
|
// edge case: mod has installed for forge but the instance is fabric => fabric version will be prioritizated on update
|
||||||
for (auto l : instanceLoaders) {
|
auto currentLoaders = instanceLoaders + ModPlatform::modLoaderTypesToList(modLoaders);
|
||||||
auto ver = bestVersion(l);
|
currentLoaders.append(noLoader); // add a fallback in case the versions do not define a loader
|
||||||
if (ver.has_value()) {
|
|
||||||
return ver;
|
for (auto loader : currentLoaders) {
|
||||||
|
if (bestMatch.contains(loader)) {
|
||||||
|
auto bestForLoader = bestMatch.value(loader);
|
||||||
|
// awkward case where the mod has only two loaders and one of them is not specified
|
||||||
|
if (loader != noLoader && bestMatch.contains(noLoader) && bestMatch.size() == 2) {
|
||||||
|
auto bestForNoLoader = bestMatch.value(noLoader);
|
||||||
|
if (bestForNoLoader.date > bestForLoader.date) {
|
||||||
|
return bestForNoLoader;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestForLoader;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return bestVersion(modLoaders);
|
return {};
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "Application.h"
|
|
||||||
#include "modplatform/CheckUpdateTask.h"
|
#include "modplatform/CheckUpdateTask.h"
|
||||||
#include "net/NetJob.h"
|
#include "net/NetJob.h"
|
||||||
|
|
||||||
|
@ -75,12 +75,12 @@ bool FlameCreationTask::abort()
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
m_abort = true;
|
m_abort = true;
|
||||||
if (m_process_update_file_info_job)
|
if (m_processUpdateFileInfoJob)
|
||||||
m_process_update_file_info_job->abort();
|
m_processUpdateFileInfoJob->abort();
|
||||||
if (m_files_job)
|
if (m_filesJob)
|
||||||
m_files_job->abort();
|
m_filesJob->abort();
|
||||||
if (m_mod_id_resolver)
|
if (m_modIdResolver)
|
||||||
m_mod_id_resolver->abort();
|
m_modIdResolver->abort();
|
||||||
|
|
||||||
return Task::abort();
|
return Task::abort();
|
||||||
}
|
}
|
||||||
@ -232,12 +232,12 @@ bool FlameCreationTask::updateInstance()
|
|||||||
connect(job.get(), &Task::failed, this, [](QString reason) { qCritical() << "Failed to get files: " << reason; });
|
connect(job.get(), &Task::failed, this, [](QString reason) { qCritical() << "Failed to get files: " << reason; });
|
||||||
connect(job.get(), &Task::finished, &loop, &QEventLoop::quit);
|
connect(job.get(), &Task::finished, &loop, &QEventLoop::quit);
|
||||||
|
|
||||||
m_process_update_file_info_job = job;
|
m_processUpdateFileInfoJob = job;
|
||||||
job->start();
|
job->start();
|
||||||
|
|
||||||
loop.exec();
|
loop.exec();
|
||||||
|
|
||||||
m_process_update_file_info_job = nullptr;
|
m_processUpdateFileInfoJob = nullptr;
|
||||||
} else {
|
} else {
|
||||||
// We don't have an old index file, so we may duplicate stuff!
|
// We don't have an old index file, so we may duplicate stuff!
|
||||||
auto dialog = CustomMessageBox::selectable(m_parent, tr("No index file."),
|
auto dialog = CustomMessageBox::selectable(m_parent, tr("No index file."),
|
||||||
@ -430,26 +430,26 @@ bool FlameCreationTask::createInstance()
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Don't add managed info to packs without an ID (most likely imported from ZIP)
|
// Don't add managed info to packs without an ID (most likely imported from ZIP)
|
||||||
if (!m_managed_id.isEmpty())
|
if (!m_managedId.isEmpty())
|
||||||
instance.setManagedPack("flame", m_managed_id, m_pack.name, m_managed_version_id, m_pack.version);
|
instance.setManagedPack("flame", m_managedId, m_pack.name, m_managedVersionId, m_pack.version);
|
||||||
else
|
else
|
||||||
instance.setManagedPack("flame", "", name(), "", "");
|
instance.setManagedPack("flame", "", name(), "", "");
|
||||||
|
|
||||||
instance.setName(name());
|
instance.setName(name());
|
||||||
|
|
||||||
m_mod_id_resolver.reset(new Flame::FileResolvingTask(APPLICATION->network(), m_pack));
|
m_modIdResolver.reset(new Flame::FileResolvingTask(APPLICATION->network(), m_pack));
|
||||||
connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::succeeded, this, [this, &loop] { idResolverSucceeded(loop); });
|
connect(m_modIdResolver.get(), &Flame::FileResolvingTask::succeeded, this, [this, &loop] { idResolverSucceeded(loop); });
|
||||||
connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::failed, [this, &loop](QString reason) {
|
connect(m_modIdResolver.get(), &Flame::FileResolvingTask::failed, [this, &loop](QString reason) {
|
||||||
m_mod_id_resolver.reset();
|
m_modIdResolver.reset();
|
||||||
setError(tr("Unable to resolve mod IDs:\n") + reason);
|
setError(tr("Unable to resolve mod IDs:\n") + reason);
|
||||||
loop.quit();
|
loop.quit();
|
||||||
});
|
});
|
||||||
connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::aborted, &loop, &QEventLoop::quit);
|
connect(m_modIdResolver.get(), &Flame::FileResolvingTask::aborted, &loop, &QEventLoop::quit);
|
||||||
connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::progress, this, &FlameCreationTask::setProgress);
|
connect(m_modIdResolver.get(), &Flame::FileResolvingTask::progress, this, &FlameCreationTask::setProgress);
|
||||||
connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::status, this, &FlameCreationTask::setStatus);
|
connect(m_modIdResolver.get(), &Flame::FileResolvingTask::status, this, &FlameCreationTask::setStatus);
|
||||||
connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::stepProgress, this, &FlameCreationTask::propagateStepProgress);
|
connect(m_modIdResolver.get(), &Flame::FileResolvingTask::stepProgress, this, &FlameCreationTask::propagateStepProgress);
|
||||||
connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::details, this, &FlameCreationTask::setDetails);
|
connect(m_modIdResolver.get(), &Flame::FileResolvingTask::details, this, &FlameCreationTask::setDetails);
|
||||||
m_mod_id_resolver->start();
|
m_modIdResolver->start();
|
||||||
|
|
||||||
loop.exec();
|
loop.exec();
|
||||||
|
|
||||||
@ -468,14 +468,14 @@ bool FlameCreationTask::createInstance()
|
|||||||
|
|
||||||
void FlameCreationTask::idResolverSucceeded(QEventLoop& loop)
|
void FlameCreationTask::idResolverSucceeded(QEventLoop& loop)
|
||||||
{
|
{
|
||||||
auto results = m_mod_id_resolver->getResults();
|
auto results = m_modIdResolver->getResults();
|
||||||
|
|
||||||
// first check for blocked mods
|
// first check for blocked mods
|
||||||
QList<BlockedMod> blocked_mods;
|
QList<BlockedMod> blocked_mods;
|
||||||
auto anyBlocked = false;
|
auto anyBlocked = false;
|
||||||
for (const auto& result : results.files.values()) {
|
for (const auto& result : results.files.values()) {
|
||||||
if (result.version.fileName.endsWith(".zip")) {
|
if (result.resourceType != PackedResourceType::Mod) {
|
||||||
m_ZIP_resources.append(std::make_pair(result.version.fileName, result.targetFolder));
|
m_otherResources.append(std::make_pair(result.version.fileName, result.targetFolder));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.version.downloadUrl.isEmpty()) {
|
if (result.version.downloadUrl.isEmpty()) {
|
||||||
@ -507,7 +507,7 @@ void FlameCreationTask::idResolverSucceeded(QEventLoop& loop)
|
|||||||
copyBlockedMods(blocked_mods);
|
copyBlockedMods(blocked_mods);
|
||||||
setupDownloadJob(loop);
|
setupDownloadJob(loop);
|
||||||
} else {
|
} else {
|
||||||
m_mod_id_resolver.reset();
|
m_modIdResolver.reset();
|
||||||
setError("Canceled");
|
setError("Canceled");
|
||||||
loop.quit();
|
loop.quit();
|
||||||
}
|
}
|
||||||
@ -518,8 +518,8 @@ void FlameCreationTask::idResolverSucceeded(QEventLoop& loop)
|
|||||||
|
|
||||||
void FlameCreationTask::setupDownloadJob(QEventLoop& loop)
|
void FlameCreationTask::setupDownloadJob(QEventLoop& loop)
|
||||||
{
|
{
|
||||||
m_files_job.reset(new NetJob(tr("Mod Download Flame"), APPLICATION->network()));
|
m_filesJob.reset(new NetJob(tr("Mod Download Flame"), APPLICATION->network()));
|
||||||
auto results = m_mod_id_resolver->getResults().files;
|
auto results = m_modIdResolver->getResults().files;
|
||||||
|
|
||||||
QStringList optionalFiles;
|
QStringList optionalFiles;
|
||||||
for (auto& result : results) {
|
for (auto& result : results) {
|
||||||
@ -554,26 +554,26 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop)
|
|||||||
if (!result.version.downloadUrl.isEmpty()) {
|
if (!result.version.downloadUrl.isEmpty()) {
|
||||||
qDebug() << "Will download" << result.version.downloadUrl << "to" << path;
|
qDebug() << "Will download" << result.version.downloadUrl << "to" << path;
|
||||||
auto dl = Net::ApiDownload::makeFile(result.version.downloadUrl, path);
|
auto dl = Net::ApiDownload::makeFile(result.version.downloadUrl, path);
|
||||||
m_files_job->addNetAction(dl);
|
m_filesJob->addNetAction(dl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
connect(m_files_job.get(), &NetJob::finished, this, [this, &loop]() {
|
connect(m_filesJob.get(), &NetJob::finished, this, [this, &loop]() {
|
||||||
m_files_job.reset();
|
m_filesJob.reset();
|
||||||
validateZIPResources(loop);
|
validateOtherResources(loop);
|
||||||
});
|
});
|
||||||
connect(m_files_job.get(), &NetJob::failed, [this](QString reason) {
|
connect(m_filesJob.get(), &NetJob::failed, [this](QString reason) {
|
||||||
m_files_job.reset();
|
m_filesJob.reset();
|
||||||
setError(reason);
|
setError(reason);
|
||||||
});
|
});
|
||||||
connect(m_files_job.get(), &NetJob::progress, this, [this](qint64 current, qint64 total) {
|
connect(m_filesJob.get(), &NetJob::progress, this, [this](qint64 current, qint64 total) {
|
||||||
setDetails(tr("%1 out of %2 complete").arg(current).arg(total));
|
setDetails(tr("%1 out of %2 complete").arg(current).arg(total));
|
||||||
setProgress(current, total);
|
setProgress(current, total);
|
||||||
});
|
});
|
||||||
connect(m_files_job.get(), &NetJob::stepProgress, this, &FlameCreationTask::propagateStepProgress);
|
connect(m_filesJob.get(), &NetJob::stepProgress, this, &FlameCreationTask::propagateStepProgress);
|
||||||
|
|
||||||
setStatus(tr("Downloading mods..."));
|
setStatus(tr("Downloading mods..."));
|
||||||
m_files_job->start();
|
m_filesJob->start();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @brief copy the matched blocked mods to the instance staging area
|
/// @brief copy the matched blocked mods to the instance staging area
|
||||||
@ -597,8 +597,14 @@ void FlameCreationTask::copyBlockedMods(QList<BlockedMod> const& blocked_mods)
|
|||||||
|
|
||||||
qDebug() << "Will try to copy" << mod.localPath << "to" << destPath;
|
qDebug() << "Will try to copy" << mod.localPath << "to" << destPath;
|
||||||
|
|
||||||
if (!FS::copy(mod.localPath, destPath)()) {
|
if (mod.move) {
|
||||||
qDebug() << "Copy of" << mod.localPath << "to" << destPath << "Failed";
|
if (!FS::move(mod.localPath, destPath)) {
|
||||||
|
qDebug() << "Move of" << mod.localPath << "to" << destPath << "Failed";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!FS::copy(mod.localPath, destPath)()) {
|
||||||
|
qDebug() << "Copy of" << mod.localPath << "to" << destPath << "Failed";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
i++;
|
i++;
|
||||||
@ -608,11 +614,11 @@ void FlameCreationTask::copyBlockedMods(QList<BlockedMod> const& blocked_mods)
|
|||||||
setAbortable(true);
|
setAbortable(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void FlameCreationTask::validateZIPResources(QEventLoop& loop)
|
void FlameCreationTask::validateOtherResources(QEventLoop& loop)
|
||||||
{
|
{
|
||||||
qDebug() << "Validating whether resources stored as .zip are in the right place";
|
qDebug() << "Validating whether other resources are in the right place";
|
||||||
QStringList zipMods;
|
QStringList zipMods;
|
||||||
for (auto [fileName, targetFolder] : m_ZIP_resources) {
|
for (auto [fileName, targetFolder] : m_otherResources) {
|
||||||
qDebug() << "Checking" << fileName << "...";
|
qDebug() << "Checking" << fileName << "...";
|
||||||
auto localPath = FS::PathCombine(m_stagingPath, "minecraft", targetFolder, fileName);
|
auto localPath = FS::PathCombine(m_stagingPath, "minecraft", targetFolder, fileName);
|
||||||
|
|
||||||
@ -672,6 +678,7 @@ void FlameCreationTask::validateZIPResources(QEventLoop& loop)
|
|||||||
installWorld(worldPath);
|
installWorld(worldPath);
|
||||||
break;
|
break;
|
||||||
case PackedResourceType::UNKNOWN:
|
case PackedResourceType::UNKNOWN:
|
||||||
|
/* fallthrough */
|
||||||
default:
|
default:
|
||||||
qDebug() << "Can't Identify" << fileName << "at" << localPath << ", leaving it where it is.";
|
qDebug() << "Can't Identify" << fileName << "at" << localPath << ", leaving it where it is.";
|
||||||
break;
|
break;
|
||||||
@ -679,7 +686,7 @@ void FlameCreationTask::validateZIPResources(QEventLoop& loop)
|
|||||||
}
|
}
|
||||||
// TODO make this work with other sorts of resource
|
// TODO make this work with other sorts of resource
|
||||||
auto task = makeShared<ConcurrentTask>("CreateModMetadata", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt());
|
auto task = makeShared<ConcurrentTask>("CreateModMetadata", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt());
|
||||||
auto results = m_mod_id_resolver->getResults().files;
|
auto results = m_modIdResolver->getResults().files;
|
||||||
auto folder = FS::PathCombine(m_stagingPath, "minecraft", "mods", ".index");
|
auto folder = FS::PathCombine(m_stagingPath, "minecraft", "mods", ".index");
|
||||||
for (auto file : results) {
|
for (auto file : results) {
|
||||||
if (file.targetFolder != "mods" || (file.version.fileName.endsWith(".zip") && !zipMods.contains(file.version.fileName))) {
|
if (file.targetFolder != "mods" || (file.version.fileName.endsWith(".zip") && !zipMods.contains(file.version.fileName))) {
|
||||||
@ -688,6 +695,6 @@ void FlameCreationTask::validateZIPResources(QEventLoop& loop)
|
|||||||
task->addTask(makeShared<LocalResourceUpdateTask>(folder, file.pack, file.version));
|
task->addTask(makeShared<LocalResourceUpdateTask>(folder, file.pack, file.version));
|
||||||
}
|
}
|
||||||
connect(task.get(), &Task::finished, &loop, &QEventLoop::quit);
|
connect(task.get(), &Task::finished, &loop, &QEventLoop::quit);
|
||||||
m_process_update_file_info_job = task;
|
m_processUpdateFileInfoJob = task;
|
||||||
task->start();
|
task->start();
|
||||||
}
|
}
|
||||||
|
@ -57,7 +57,7 @@ class FlameCreationTask final : public InstanceCreationTask {
|
|||||||
QString id,
|
QString id,
|
||||||
QString version_id,
|
QString version_id,
|
||||||
QString original_instance_id = {})
|
QString original_instance_id = {})
|
||||||
: InstanceCreationTask(), m_parent(parent), m_managed_id(std::move(id)), m_managed_version_id(std::move(version_id))
|
: InstanceCreationTask(), m_parent(parent), m_managedId(std::move(id)), m_managedVersionId(std::move(version_id))
|
||||||
{
|
{
|
||||||
setStagingPath(staging_path);
|
setStagingPath(staging_path);
|
||||||
setParentSettings(global_settings);
|
setParentSettings(global_settings);
|
||||||
@ -74,22 +74,22 @@ class FlameCreationTask final : public InstanceCreationTask {
|
|||||||
void idResolverSucceeded(QEventLoop&);
|
void idResolverSucceeded(QEventLoop&);
|
||||||
void setupDownloadJob(QEventLoop&);
|
void setupDownloadJob(QEventLoop&);
|
||||||
void copyBlockedMods(QList<BlockedMod> const& blocked_mods);
|
void copyBlockedMods(QList<BlockedMod> const& blocked_mods);
|
||||||
void validateZIPResources(QEventLoop& loop);
|
void validateOtherResources(QEventLoop& loop);
|
||||||
QString getVersionForLoader(QString uid, QString loaderType, QString version, QString mcVersion);
|
QString getVersionForLoader(QString uid, QString loaderType, QString version, QString mcVersion);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QWidget* m_parent = nullptr;
|
QWidget* m_parent = nullptr;
|
||||||
|
|
||||||
shared_qobject_ptr<Flame::FileResolvingTask> m_mod_id_resolver;
|
shared_qobject_ptr<Flame::FileResolvingTask> m_modIdResolver;
|
||||||
Flame::Manifest m_pack;
|
Flame::Manifest m_pack;
|
||||||
|
|
||||||
// Handle to allow aborting
|
// Handle to allow aborting
|
||||||
Task::Ptr m_process_update_file_info_job = nullptr;
|
Task::Ptr m_processUpdateFileInfoJob = nullptr;
|
||||||
NetJob::Ptr m_files_job = nullptr;
|
NetJob::Ptr m_filesJob = nullptr;
|
||||||
|
|
||||||
QString m_managed_id, m_managed_version_id;
|
QString m_managedId, m_managedVersionId;
|
||||||
|
|
||||||
QList<std::pair<QString, QString>> m_ZIP_resources;
|
QList<std::pair<QString, QString>> m_otherResources;
|
||||||
|
|
||||||
std::optional<InstancePtr> m_instance;
|
std::optional<InstancePtr> m_instance;
|
||||||
};
|
};
|
||||||
|
@ -40,6 +40,7 @@
|
|||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
#include <QVector>
|
#include <QVector>
|
||||||
|
#include "minecraft/mod/tasks/LocalResourceParse.h"
|
||||||
#include "modplatform/ModIndex.h"
|
#include "modplatform/ModIndex.h"
|
||||||
|
|
||||||
namespace Flame {
|
namespace Flame {
|
||||||
@ -54,6 +55,7 @@ struct File {
|
|||||||
|
|
||||||
// our
|
// our
|
||||||
QString targetFolder = QStringLiteral("mods");
|
QString targetFolder = QStringLiteral("mods");
|
||||||
|
PackedResourceType resourceType;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Modloader {
|
struct Modloader {
|
||||||
|
@ -71,13 +71,15 @@ class ModrinthAPI : public NetworkResourceAPI {
|
|||||||
|
|
||||||
static auto getSideFilters(QString side) -> const QString
|
static auto getSideFilters(QString side) -> const QString
|
||||||
{
|
{
|
||||||
if (side.isEmpty() || side == "both") {
|
if (side.isEmpty()) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
if (side == "both")
|
||||||
|
return QString("\"client_side:required\"],[\"server_side:required\"");
|
||||||
if (side == "client")
|
if (side == "client")
|
||||||
return QString("\"client_side:required\",\"client_side:optional\"");
|
return QString("\"client_side:required\",\"client_side:optional\"],[\"server_side:optional\",\"server_side:unsupported\"");
|
||||||
if (side == "server")
|
if (side == "server")
|
||||||
return QString("\"server_side:required\",\"server_side:optional\"");
|
return QString("\"server_side:required\",\"server_side:optional\"],[\"client_side:optional\",\"client_side:unsupported\"");
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
#include "QObjectPtr.h"
|
#include "QObjectPtr.h"
|
||||||
#include "ResourceDownloadTask.h"
|
#include "ResourceDownloadTask.h"
|
||||||
|
|
||||||
|
#include "modplatform/ModIndex.h"
|
||||||
#include "modplatform/helpers/HashUtils.h"
|
#include "modplatform/helpers/HashUtils.h"
|
||||||
|
|
||||||
#include "tasks/ConcurrentTask.h"
|
#include "tasks/ConcurrentTask.h"
|
||||||
@ -107,10 +108,8 @@ void ModrinthCheckUpdate::checkVersionsResponse(std::shared_ptr<QByteArray> resp
|
|||||||
// Sometimes a version may have multiple files, one with "forge" and one with "fabric",
|
// Sometimes a version may have multiple files, one with "forge" and one with "fabric",
|
||||||
// so we may want to filter it
|
// so we may want to filter it
|
||||||
QString loader_filter;
|
QString loader_filter;
|
||||||
static auto flags = { ModPlatform::ModLoaderType::NeoForge, ModPlatform::ModLoaderType::Forge,
|
if (loader.has_value()) {
|
||||||
ModPlatform::ModLoaderType::Quilt, ModPlatform::ModLoaderType::Fabric };
|
for (auto flag : ModPlatform::modLoaderTypesToList(*loader)) {
|
||||||
for (auto flag : flags) {
|
|
||||||
if (loader.has_value() && loader->testFlag(flag)) {
|
|
||||||
loader_filter = ModPlatform::getModLoaderAsString(flag);
|
loader_filter = ModPlatform::getModLoaderAsString(flag);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -262,12 +262,14 @@ bool ModrinthCreationTask::createInstance()
|
|||||||
mod->setDetails(d);
|
mod->setDetails(d);
|
||||||
resources[file.hash.toHex()] = mod;
|
resources[file.hash.toHex()] = mod;
|
||||||
}
|
}
|
||||||
|
if (file.downloads.empty()) {
|
||||||
|
setError(tr("The file '%1' is missing a download link. This is invalid in the pack format.").arg(fileName));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
qDebug() << "Will try to download" << file.downloads.front() << "to" << file_path;
|
qDebug() << "Will try to download" << file.downloads.front() << "to" << file_path;
|
||||||
auto dl = Net::ApiDownload::makeFile(file.downloads.dequeue(), file_path);
|
auto dl = Net::ApiDownload::makeFile(file.downloads.dequeue(), file_path);
|
||||||
dl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash));
|
dl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash));
|
||||||
downloadMods->addNetAction(dl);
|
downloadMods->addNetAction(dl);
|
||||||
|
|
||||||
if (!file.downloads.empty()) {
|
if (!file.downloads.empty()) {
|
||||||
// FIXME: This really needs to be put into a ConcurrentTask of
|
// FIXME: This really needs to be put into a ConcurrentTask of
|
||||||
// MultipleOptionsTask's , once those exist :)
|
// MultipleOptionsTask's , once those exist :)
|
||||||
|
@ -190,11 +190,8 @@ void V1::updateModIndex(const QDir& index_dir, Mod& mod)
|
|||||||
}
|
}
|
||||||
|
|
||||||
toml::array loaders;
|
toml::array loaders;
|
||||||
for (auto loader : { ModPlatform::NeoForge, ModPlatform::Forge, ModPlatform::Cauldron, ModPlatform::LiteLoader, ModPlatform::Fabric,
|
for (auto loader : ModPlatform::modLoaderTypesToList(mod.loaders)) {
|
||||||
ModPlatform::Quilt }) {
|
loaders.push_back(getModLoaderAsString(loader).toStdString());
|
||||||
if (mod.loaders & loader) {
|
|
||||||
loaders.push_back(getModLoaderAsString(loader).toStdString());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
toml::array mcVersions;
|
toml::array mcVersions;
|
||||||
for (auto version : mod.mcVersions) {
|
for (auto version : mod.mcVersions) {
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
[Icon Theme]
|
[Icon Theme]
|
||||||
Name=Legacy
|
Name=Legacy
|
||||||
Comment=Default Icons
|
Comment=Default Icons
|
||||||
Inherits=default
|
|
||||||
Directories=8x8,16x16,22x22,24x24,32x32,32x32/instances,48x48,50x50/instances,64x64,128x128/instances,256x256,scalable,scalable/instances
|
Directories=8x8,16x16,22x22,24x24,32x32,32x32/instances,48x48,50x50/instances,64x64,128x128/instances,256x256,scalable,scalable/instances
|
||||||
|
|
||||||
[8x8]
|
[8x8]
|
||||||
|
@ -51,11 +51,35 @@
|
|||||||
#include <settings/SettingsObject.h>
|
#include <settings/SettingsObject.h>
|
||||||
#include "Application.h"
|
#include "Application.h"
|
||||||
|
|
||||||
|
constexpr int MaxMclogsLines = 25000;
|
||||||
|
constexpr int InitialMclogsLines = 10000;
|
||||||
|
constexpr int FinalMclogsLines = 14900;
|
||||||
|
|
||||||
|
QString truncateLogForMclogs(const QString& logContent)
|
||||||
|
{
|
||||||
|
QStringList lines = logContent.split("\n");
|
||||||
|
if (lines.size() > MaxMclogsLines) {
|
||||||
|
QString truncatedLog = lines.mid(0, InitialMclogsLines).join("\n");
|
||||||
|
truncatedLog +=
|
||||||
|
"\n\n\n\n\n\n\n\n\n\n"
|
||||||
|
"------------------------------------------------------------\n"
|
||||||
|
"----------------------- Log truncated ----------------------\n"
|
||||||
|
"------------------------------------------------------------\n"
|
||||||
|
"----- Middle portion omitted to fit mclo.gs size limits ----\n"
|
||||||
|
"------------------------------------------------------------\n"
|
||||||
|
"\n\n\n\n\n\n\n\n\n\n";
|
||||||
|
truncatedLog += lines.mid(lines.size() - FinalMclogsLines - 1).join("\n");
|
||||||
|
return truncatedLog;
|
||||||
|
}
|
||||||
|
return logContent;
|
||||||
|
}
|
||||||
|
|
||||||
std::optional<QString> GuiUtil::uploadPaste(const QString& name, const QString& text, QWidget* parentWidget)
|
std::optional<QString> GuiUtil::uploadPaste(const QString& name, const QString& text, QWidget* parentWidget)
|
||||||
{
|
{
|
||||||
ProgressDialog dialog(parentWidget);
|
ProgressDialog dialog(parentWidget);
|
||||||
auto pasteTypeSetting = static_cast<PasteUpload::PasteType>(APPLICATION->settings()->get("PastebinType").toInt());
|
auto pasteTypeSetting = static_cast<PasteUpload::PasteType>(APPLICATION->settings()->get("PastebinType").toInt());
|
||||||
auto pasteCustomAPIBaseSetting = APPLICATION->settings()->get("PastebinCustomAPIBase").toString();
|
auto pasteCustomAPIBaseSetting = APPLICATION->settings()->get("PastebinCustomAPIBase").toString();
|
||||||
|
bool shouldTruncate = false;
|
||||||
|
|
||||||
{
|
{
|
||||||
QUrl baseUrl;
|
QUrl baseUrl;
|
||||||
@ -75,10 +99,36 @@ std::optional<QString> GuiUtil::uploadPaste(const QString& name, const QString&
|
|||||||
|
|
||||||
if (response != QMessageBox::Yes)
|
if (response != QMessageBox::Yes)
|
||||||
return {};
|
return {};
|
||||||
|
|
||||||
|
if (baseUrl.toString() == "https://api.mclo.gs" && text.count("\n") > MaxMclogsLines) {
|
||||||
|
auto truncateResponse = CustomMessageBox::selectable(
|
||||||
|
parentWidget, QObject::tr("Confirm Truncation"),
|
||||||
|
QObject::tr("The log has %1 lines, exceeding mclo.gs' limit of %2.\n"
|
||||||
|
"The launcher can keep the first %3 and last %4 lines, trimming the middle.\n\n"
|
||||||
|
"If you choose 'No', mclo.gs will only keep the first %2 lines, cutting off "
|
||||||
|
"potentially useful info like crashes at the end.\n\n"
|
||||||
|
"Proceed with truncation?")
|
||||||
|
.arg(text.count("\n"))
|
||||||
|
.arg(MaxMclogsLines)
|
||||||
|
.arg(InitialMclogsLines)
|
||||||
|
.arg(FinalMclogsLines),
|
||||||
|
QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, QMessageBox::No)
|
||||||
|
->exec();
|
||||||
|
|
||||||
|
if (truncateResponse == QMessageBox::Cancel) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
shouldTruncate = truncateResponse == QMessageBox::Yes;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<PasteUpload> paste(new PasteUpload(parentWidget, text, pasteCustomAPIBaseSetting, pasteTypeSetting));
|
QString textToUpload = text;
|
||||||
|
if (shouldTruncate) {
|
||||||
|
textToUpload = truncateLogForMclogs(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<PasteUpload> paste(new PasteUpload(parentWidget, textToUpload, pasteCustomAPIBaseSetting, pasteTypeSetting));
|
||||||
|
|
||||||
dialog.execWithTask(paste.get());
|
dialog.execWithTask(paste.get());
|
||||||
if (!paste->wasSuccessful()) {
|
if (!paste->wasSuccessful()) {
|
||||||
|
@ -288,6 +288,8 @@ void BlockedModsDialog::checkMatchHash(QString hash, QString path)
|
|||||||
|
|
||||||
qDebug() << "[Blocked Mods Dialog] Checking for match on hash: " << hash << "| From path:" << path;
|
qDebug() << "[Blocked Mods Dialog] Checking for match on hash: " << hash << "| From path:" << path;
|
||||||
|
|
||||||
|
auto downloadDir = QFileInfo(APPLICATION->settings()->get("DownloadsDir").toString()).absoluteFilePath();
|
||||||
|
auto moveFiles = APPLICATION->settings()->get("MoveModsFromDownloadsDir").toBool();
|
||||||
for (auto& mod : m_mods) {
|
for (auto& mod : m_mods) {
|
||||||
if (mod.matched) {
|
if (mod.matched) {
|
||||||
continue;
|
continue;
|
||||||
@ -295,6 +297,9 @@ void BlockedModsDialog::checkMatchHash(QString hash, QString path)
|
|||||||
if (mod.hash.compare(hash, Qt::CaseInsensitive) == 0) {
|
if (mod.hash.compare(hash, Qt::CaseInsensitive) == 0) {
|
||||||
mod.matched = true;
|
mod.matched = true;
|
||||||
mod.localPath = path;
|
mod.localPath = path;
|
||||||
|
if (moveFiles) {
|
||||||
|
mod.move = QFileInfo(path).absoluteFilePath().startsWith(downloadDir);
|
||||||
|
}
|
||||||
match = true;
|
match = true;
|
||||||
|
|
||||||
qDebug() << "[Blocked Mods Dialog] Hash match found:" << mod.name << hash << "| From path:" << path;
|
qDebug() << "[Blocked Mods Dialog] Hash match found:" << mod.name << hash << "| From path:" << path;
|
||||||
@ -346,6 +351,8 @@ bool BlockedModsDialog::checkValidPath(QString path)
|
|||||||
return fsName.compare(metaName) == 0;
|
return fsName.compare(metaName) == 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
auto downloadDir = QFileInfo(APPLICATION->settings()->get("DownloadsDir").toString()).absoluteFilePath();
|
||||||
|
auto moveFiles = APPLICATION->settings()->get("MoveModsFromDownloadsDir").toBool();
|
||||||
for (auto& mod : m_mods) {
|
for (auto& mod : m_mods) {
|
||||||
if (compare(filename, mod.name)) {
|
if (compare(filename, mod.name)) {
|
||||||
// if the mod is not yet matched and doesn't have a hash then
|
// if the mod is not yet matched and doesn't have a hash then
|
||||||
@ -353,6 +360,9 @@ bool BlockedModsDialog::checkValidPath(QString path)
|
|||||||
if (!mod.matched && mod.hash.isEmpty()) {
|
if (!mod.matched && mod.hash.isEmpty()) {
|
||||||
mod.matched = true;
|
mod.matched = true;
|
||||||
mod.localPath = path;
|
mod.localPath = path;
|
||||||
|
if (moveFiles) {
|
||||||
|
mod.move = QFileInfo(path).absoluteFilePath().startsWith(downloadDir);
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
qDebug() << "[Blocked Mods Dialog] Name match found:" << mod.name << "| From path:" << path;
|
qDebug() << "[Blocked Mods Dialog] Name match found:" << mod.name << "| From path:" << path;
|
||||||
|
@ -42,6 +42,7 @@ struct BlockedMod {
|
|||||||
bool matched;
|
bool matched;
|
||||||
QString localPath;
|
QString localPath;
|
||||||
QString targetFolder;
|
QString targetFolder;
|
||||||
|
bool move = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
QT_BEGIN_NAMESPACE
|
QT_BEGIN_NAMESPACE
|
||||||
|
@ -1,64 +0,0 @@
|
|||||||
/* Copyright 2013-2021 MultiMC Contributors
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "EditAccountDialog.h"
|
|
||||||
#include <DesktopServices.h>
|
|
||||||
#include <QPushButton>
|
|
||||||
#include <QUrl>
|
|
||||||
#include "ui_EditAccountDialog.h"
|
|
||||||
|
|
||||||
EditAccountDialog::EditAccountDialog(const QString& text, QWidget* parent, int flags) : QDialog(parent), ui(new Ui::EditAccountDialog)
|
|
||||||
{
|
|
||||||
ui->setupUi(this);
|
|
||||||
|
|
||||||
ui->label->setText(text);
|
|
||||||
ui->label->setVisible(!text.isEmpty());
|
|
||||||
|
|
||||||
ui->userTextBox->setEnabled(flags & UsernameField);
|
|
||||||
ui->passTextBox->setEnabled(flags & PasswordField);
|
|
||||||
|
|
||||||
ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel"));
|
|
||||||
ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK"));
|
|
||||||
}
|
|
||||||
|
|
||||||
EditAccountDialog::~EditAccountDialog()
|
|
||||||
{
|
|
||||||
delete ui;
|
|
||||||
}
|
|
||||||
|
|
||||||
void EditAccountDialog::on_label_linkActivated(const QString& link)
|
|
||||||
{
|
|
||||||
DesktopServices::openUrl(QUrl(link));
|
|
||||||
}
|
|
||||||
|
|
||||||
void EditAccountDialog::setUsername(const QString& user) const
|
|
||||||
{
|
|
||||||
ui->userTextBox->setText(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
QString EditAccountDialog::username() const
|
|
||||||
{
|
|
||||||
return ui->userTextBox->text();
|
|
||||||
}
|
|
||||||
|
|
||||||
void EditAccountDialog::setPassword(const QString& pass) const
|
|
||||||
{
|
|
||||||
ui->passTextBox->setText(pass);
|
|
||||||
}
|
|
||||||
|
|
||||||
QString EditAccountDialog::password() const
|
|
||||||
{
|
|
||||||
return ui->passTextBox->text();
|
|
||||||
}
|
|
@ -1,52 +0,0 @@
|
|||||||
/* Copyright 2013-2021 MultiMC Contributors
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QDialog>
|
|
||||||
|
|
||||||
namespace Ui {
|
|
||||||
class EditAccountDialog;
|
|
||||||
}
|
|
||||||
|
|
||||||
class EditAccountDialog : public QDialog {
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit EditAccountDialog(const QString& text = "", QWidget* parent = 0, int flags = UsernameField | PasswordField);
|
|
||||||
~EditAccountDialog();
|
|
||||||
|
|
||||||
void setUsername(const QString& user) const;
|
|
||||||
void setPassword(const QString& pass) const;
|
|
||||||
|
|
||||||
QString username() const;
|
|
||||||
QString password() const;
|
|
||||||
|
|
||||||
enum Flags {
|
|
||||||
NoFlags = 0,
|
|
||||||
|
|
||||||
//! Specifies that the dialog should have a username field.
|
|
||||||
UsernameField,
|
|
||||||
|
|
||||||
//! Specifies that the dialog should have a password field.
|
|
||||||
PasswordField,
|
|
||||||
};
|
|
||||||
|
|
||||||
private slots:
|
|
||||||
void on_label_linkActivated(const QString& link);
|
|
||||||
|
|
||||||
private:
|
|
||||||
Ui::EditAccountDialog* ui;
|
|
||||||
};
|
|
@ -1,94 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<ui version="4.0">
|
|
||||||
<class>EditAccountDialog</class>
|
|
||||||
<widget class="QDialog" name="EditAccountDialog">
|
|
||||||
<property name="geometry">
|
|
||||||
<rect>
|
|
||||||
<x>0</x>
|
|
||||||
<y>0</y>
|
|
||||||
<width>400</width>
|
|
||||||
<height>148</height>
|
|
||||||
</rect>
|
|
||||||
</property>
|
|
||||||
<property name="windowTitle">
|
|
||||||
<string>Login</string>
|
|
||||||
</property>
|
|
||||||
<layout class="QVBoxLayout" name="verticalLayout">
|
|
||||||
<item>
|
|
||||||
<widget class="QLabel" name="label">
|
|
||||||
<property name="text">
|
|
||||||
<string notr="true">Message label placeholder.</string>
|
|
||||||
</property>
|
|
||||||
<property name="textFormat">
|
|
||||||
<enum>Qt::RichText</enum>
|
|
||||||
</property>
|
|
||||||
<property name="textInteractionFlags">
|
|
||||||
<set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QLineEdit" name="userTextBox">
|
|
||||||
<property name="placeholderText">
|
|
||||||
<string>Email</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QLineEdit" name="passTextBox">
|
|
||||||
<property name="echoMode">
|
|
||||||
<enum>QLineEdit::Password</enum>
|
|
||||||
</property>
|
|
||||||
<property name="placeholderText">
|
|
||||||
<string>Password</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QDialogButtonBox" name="buttonBox">
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Horizontal</enum>
|
|
||||||
</property>
|
|
||||||
<property name="standardButtons">
|
|
||||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</widget>
|
|
||||||
<resources/>
|
|
||||||
<connections>
|
|
||||||
<connection>
|
|
||||||
<sender>buttonBox</sender>
|
|
||||||
<signal>accepted()</signal>
|
|
||||||
<receiver>EditAccountDialog</receiver>
|
|
||||||
<slot>accept()</slot>
|
|
||||||
<hints>
|
|
||||||
<hint type="sourcelabel">
|
|
||||||
<x>248</x>
|
|
||||||
<y>254</y>
|
|
||||||
</hint>
|
|
||||||
<hint type="destinationlabel">
|
|
||||||
<x>157</x>
|
|
||||||
<y>274</y>
|
|
||||||
</hint>
|
|
||||||
</hints>
|
|
||||||
</connection>
|
|
||||||
<connection>
|
|
||||||
<sender>buttonBox</sender>
|
|
||||||
<signal>rejected()</signal>
|
|
||||||
<receiver>EditAccountDialog</receiver>
|
|
||||||
<slot>reject()</slot>
|
|
||||||
<hints>
|
|
||||||
<hint type="sourcelabel">
|
|
||||||
<x>316</x>
|
|
||||||
<y>260</y>
|
|
||||||
</hint>
|
|
||||||
<hint type="destinationlabel">
|
|
||||||
<x>286</x>
|
|
||||||
<y>274</y>
|
|
||||||
</hint>
|
|
||||||
</hints>
|
|
||||||
</connection>
|
|
||||||
</connections>
|
|
||||||
</ui>
|
|
@ -15,7 +15,9 @@
|
|||||||
|
|
||||||
#include <QFileDialog>
|
#include <QFileDialog>
|
||||||
#include <QKeyEvent>
|
#include <QKeyEvent>
|
||||||
|
#include <QLineEdit>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
|
#include <QSortFilterProxyModel>
|
||||||
|
|
||||||
#include "Application.h"
|
#include "Application.h"
|
||||||
|
|
||||||
@ -33,6 +35,15 @@ IconPickerDialog::IconPickerDialog(QWidget* parent) : QDialog(parent), ui(new Ui
|
|||||||
ui->setupUi(this);
|
ui->setupUi(this);
|
||||||
setWindowModality(Qt::WindowModal);
|
setWindowModality(Qt::WindowModal);
|
||||||
|
|
||||||
|
searchBar = new QLineEdit(this);
|
||||||
|
searchBar->setPlaceholderText(tr("Search..."));
|
||||||
|
ui->verticalLayout->insertWidget(0, searchBar);
|
||||||
|
|
||||||
|
proxyModel = new QSortFilterProxyModel(this);
|
||||||
|
proxyModel->setSourceModel(APPLICATION->icons().get());
|
||||||
|
proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
|
||||||
|
ui->iconView->setModel(proxyModel);
|
||||||
|
|
||||||
auto contentsWidget = ui->iconView;
|
auto contentsWidget = ui->iconView;
|
||||||
contentsWidget->setViewMode(QListView::IconMode);
|
contentsWidget->setViewMode(QListView::IconMode);
|
||||||
contentsWidget->setFlow(QListView::LeftToRight);
|
contentsWidget->setFlow(QListView::LeftToRight);
|
||||||
@ -57,7 +68,7 @@ IconPickerDialog::IconPickerDialog(QWidget* parent) : QDialog(parent), ui(new Ui
|
|||||||
|
|
||||||
contentsWidget->installEventFilter(this);
|
contentsWidget->installEventFilter(this);
|
||||||
|
|
||||||
contentsWidget->setModel(APPLICATION->icons().get());
|
contentsWidget->setModel(proxyModel);
|
||||||
|
|
||||||
// NOTE: ResetRole forces the button to be on the left, while the OK/Cancel ones are on the right. We win.
|
// NOTE: ResetRole forces the button to be on the left, while the OK/Cancel ones are on the right. We win.
|
||||||
auto buttonAdd = ui->buttonBox->addButton(tr("Add Icon"), QDialogButtonBox::ResetRole);
|
auto buttonAdd = ui->buttonBox->addButton(tr("Add Icon"), QDialogButtonBox::ResetRole);
|
||||||
@ -76,6 +87,9 @@ IconPickerDialog::IconPickerDialog(QWidget* parent) : QDialog(parent), ui(new Ui
|
|||||||
|
|
||||||
auto buttonFolder = ui->buttonBox->addButton(tr("Open Folder"), QDialogButtonBox::ResetRole);
|
auto buttonFolder = ui->buttonBox->addButton(tr("Open Folder"), QDialogButtonBox::ResetRole);
|
||||||
connect(buttonFolder, &QPushButton::clicked, this, &IconPickerDialog::openFolder);
|
connect(buttonFolder, &QPushButton::clicked, this, &IconPickerDialog::openFolder);
|
||||||
|
connect(searchBar, &QLineEdit::textChanged, this, &IconPickerDialog::filterIcons);
|
||||||
|
// Prevent incorrect indices from e.g. filesystem changes
|
||||||
|
connect(APPLICATION->icons().get(), &IconList::iconUpdated, this, [this]() { proxyModel->invalidate(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
bool IconPickerDialog::eventFilter(QObject* obj, QEvent* evt)
|
bool IconPickerDialog::eventFilter(QObject* obj, QEvent* evt)
|
||||||
@ -162,5 +176,10 @@ IconPickerDialog::~IconPickerDialog()
|
|||||||
|
|
||||||
void IconPickerDialog::openFolder()
|
void IconPickerDialog::openFolder()
|
||||||
{
|
{
|
||||||
DesktopServices::openPath(APPLICATION->icons()->getDirectory(), true);
|
DesktopServices::openPath(APPLICATION->icons()->iconDirectory(selectedIconKey), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void IconPickerDialog::filterIcons(const QString& query)
|
||||||
|
{
|
||||||
|
proxyModel->setFilterFixedString(query);
|
||||||
|
}
|
@ -16,6 +16,8 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <QDialog>
|
#include <QDialog>
|
||||||
#include <QItemSelection>
|
#include <QItemSelection>
|
||||||
|
#include <QLineEdit>
|
||||||
|
#include <QSortFilterProxyModel>
|
||||||
|
|
||||||
namespace Ui {
|
namespace Ui {
|
||||||
class IconPickerDialog;
|
class IconPickerDialog;
|
||||||
@ -36,6 +38,8 @@ class IconPickerDialog : public QDialog {
|
|||||||
private:
|
private:
|
||||||
Ui::IconPickerDialog* ui;
|
Ui::IconPickerDialog* ui;
|
||||||
QPushButton* buttonRemove;
|
QPushButton* buttonRemove;
|
||||||
|
QLineEdit* searchBar;
|
||||||
|
QSortFilterProxyModel* proxyModel;
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void selectionChanged(QItemSelection, QItemSelection);
|
void selectionChanged(QItemSelection, QItemSelection);
|
||||||
@ -44,4 +48,5 @@ class IconPickerDialog : public QDialog {
|
|||||||
void addNewIcon();
|
void addNewIcon();
|
||||||
void removeSelectedIcon();
|
void removeSelectedIcon();
|
||||||
void openFolder();
|
void openFolder();
|
||||||
|
void filterIcons(const QString& text);
|
||||||
};
|
};
|
||||||
|
@ -30,6 +30,9 @@ Choose your name carefully:</string>
|
|||||||
<property name="wordWrap">
|
<property name="wordWrap">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="textInteractionFlags">
|
||||||
|
<set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
|
||||||
|
</property>
|
||||||
<property name="buddy">
|
<property name="buddy">
|
||||||
<cstring>nameEdit</cstring>
|
<cstring>nameEdit</cstring>
|
||||||
</property>
|
</property>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
#include "ResourceUpdateDialog.h"
|
#include "ResourceUpdateDialog.h"
|
||||||
|
#include "Application.h"
|
||||||
#include "ChooseProviderDialog.h"
|
#include "ChooseProviderDialog.h"
|
||||||
#include "CustomMessageBox.h"
|
#include "CustomMessageBox.h"
|
||||||
#include "ProgressDialog.h"
|
#include "ProgressDialog.h"
|
||||||
@ -7,6 +8,7 @@
|
|||||||
#include "minecraft/mod/tasks/GetModDependenciesTask.h"
|
#include "minecraft/mod/tasks/GetModDependenciesTask.h"
|
||||||
#include "modplatform/ModIndex.h"
|
#include "modplatform/ModIndex.h"
|
||||||
#include "modplatform/flame/FlameAPI.h"
|
#include "modplatform/flame/FlameAPI.h"
|
||||||
|
#include "tasks/SequentialTask.h"
|
||||||
#include "ui_ReviewMessageBox.h"
|
#include "ui_ReviewMessageBox.h"
|
||||||
|
|
||||||
#include "Markdown.h"
|
#include "Markdown.h"
|
||||||
@ -411,8 +413,14 @@ void ResourceUpdateDialog::onMetadataFailed(Resource* resource, bool try_others,
|
|||||||
connect(task.get(), &EnsureMetadataTask::metadataFailed, [this](Resource* candidate) { onMetadataFailed(candidate, false); });
|
connect(task.get(), &EnsureMetadataTask::metadataFailed, [this](Resource* candidate) { onMetadataFailed(candidate, false); });
|
||||||
connect(task.get(), &EnsureMetadataTask::failed,
|
connect(task.get(), &EnsureMetadataTask::failed,
|
||||||
[this](const QString& reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); });
|
[this](const QString& reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); });
|
||||||
|
if (task->getHashingTask()) {
|
||||||
m_second_try_metadata->addTask(task);
|
auto seq = makeShared<SequentialTask>();
|
||||||
|
seq->addTask(task->getHashingTask());
|
||||||
|
seq->addTask(task);
|
||||||
|
m_second_try_metadata->addTask(seq);
|
||||||
|
} else {
|
||||||
|
m_second_try_metadata->addTask(task);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
QString reason{ tr("Couldn't find a valid version on the selected mod provider(s)") };
|
QString reason{ tr("Couldn't find a valid version on the selected mod provider(s)") };
|
||||||
|
|
||||||
|
@ -207,7 +207,7 @@
|
|||||||
<item row="0" column="0">
|
<item row="0" column="0">
|
||||||
<widget class="QLabel" name="label_8">
|
<widget class="QLabel" name="label_8">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string><html><head/><body><p>Note: you only need to set this to access private data. Read the <a href="https://docs.modrinth.com/#section/Authentication">documentation</a> for more information.</p></body></html></string>
|
<string><html><head/><body><p>Note: you only need to set this to access private data. Read the <a href="https://docs.modrinth.com/api/#authentication">documentation</a> for more information.</p></body></html></string>
|
||||||
</property>
|
</property>
|
||||||
<property name="openExternalLinks">
|
<property name="openExternalLinks">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
|
@ -232,6 +232,7 @@ void LauncherPage::applySettings()
|
|||||||
s->set("SkinsDir", ui->skinsDirTextBox->text());
|
s->set("SkinsDir", ui->skinsDirTextBox->text());
|
||||||
s->set("JavaDir", ui->javaDirTextBox->text());
|
s->set("JavaDir", ui->javaDirTextBox->text());
|
||||||
s->set("DownloadsDirWatchRecursive", ui->downloadsDirWatchRecursiveCheckBox->isChecked());
|
s->set("DownloadsDirWatchRecursive", ui->downloadsDirWatchRecursiveCheckBox->isChecked());
|
||||||
|
s->set("MoveModsFromDownloadsDir", ui->downloadsDirMoveCheckBox->isChecked());
|
||||||
|
|
||||||
auto sortMode = (InstSortMode)ui->sortingModeGroup->checkedId();
|
auto sortMode = (InstSortMode)ui->sortingModeGroup->checkedId();
|
||||||
switch (sortMode) {
|
switch (sortMode) {
|
||||||
@ -296,6 +297,7 @@ void LauncherPage::loadSettings()
|
|||||||
ui->skinsDirTextBox->setText(s->get("SkinsDir").toString());
|
ui->skinsDirTextBox->setText(s->get("SkinsDir").toString());
|
||||||
ui->javaDirTextBox->setText(s->get("JavaDir").toString());
|
ui->javaDirTextBox->setText(s->get("JavaDir").toString());
|
||||||
ui->downloadsDirWatchRecursiveCheckBox->setChecked(s->get("DownloadsDirWatchRecursive").toBool());
|
ui->downloadsDirWatchRecursiveCheckBox->setChecked(s->get("DownloadsDirWatchRecursive").toBool());
|
||||||
|
ui->downloadsDirMoveCheckBox->setChecked(s->get("MoveModsFromDownloadsDir").toBool());
|
||||||
|
|
||||||
QString sortMode = s->get("InstSortMode").toString();
|
QString sortMode = s->get("InstSortMode").toString();
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>511</width>
|
<width>562</width>
|
||||||
<height>726</height>
|
<height>726</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
@ -38,7 +38,7 @@
|
|||||||
<enum>QTabWidget::Rounded</enum>
|
<enum>QTabWidget::Rounded</enum>
|
||||||
</property>
|
</property>
|
||||||
<property name="currentIndex">
|
<property name="currentIndex">
|
||||||
<number>0</number>
|
<number>2</number>
|
||||||
</property>
|
</property>
|
||||||
<widget class="QWidget" name="featuresTab">
|
<widget class="QWidget" name="featuresTab">
|
||||||
<attribute name="title">
|
<attribute name="title">
|
||||||
@ -48,7 +48,7 @@
|
|||||||
<item>
|
<item>
|
||||||
<widget class="QScrollArea" name="scrollArea">
|
<widget class="QScrollArea" name="scrollArea">
|
||||||
<property name="horizontalScrollBarPolicy">
|
<property name="horizontalScrollBarPolicy">
|
||||||
<enum>Qt::ScrollBarAlwaysOff</enum>
|
<enum>Qt::ScrollBarAsNeeded</enum>
|
||||||
</property>
|
</property>
|
||||||
<property name="widgetResizable">
|
<property name="widgetResizable">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
@ -58,8 +58,8 @@
|
|||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>473</width>
|
<width>570</width>
|
||||||
<height>690</height>
|
<height>692</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_8">
|
<layout class="QVBoxLayout" name="verticalLayout_8">
|
||||||
@ -156,14 +156,28 @@
|
|||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="9" column="1" colspan="2">
|
<item row="9" column="1" colspan="2">
|
||||||
<widget class="QCheckBox" name="downloadsDirWatchRecursiveCheckBox">
|
<layout class="QHBoxLayout" name="downloadModsCheckLayout">
|
||||||
<property name="toolTip">
|
<item>
|
||||||
<string>When enabled, in addition to the downloads folder, its sub folders will also be searched when looking for resources (e.g. when looking for blocked mods on CurseForge).</string>
|
<widget class="QCheckBox" name="downloadsDirWatchRecursiveCheckBox">
|
||||||
</property>
|
<property name="toolTip">
|
||||||
<property name="text">
|
<string>When enabled, in addition to the downloads folder, its sub folders will also be searched when looking for resources (e.g. when looking for blocked mods on CurseForge).</string>
|
||||||
<string>Check downloads folder recursively</string>
|
</property>
|
||||||
</property>
|
<property name="text">
|
||||||
</widget>
|
<string>Check downloads folder recursively</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QCheckBox" name="downloadsDirMoveCheckBox">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>When enabled, it will move blocked resources instead of copying them.</string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Move blocked resources</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item row="8" column="1">
|
<item row="8" column="1">
|
||||||
<widget class="QLineEdit" name="downloadsDirTextBox"/>
|
<widget class="QLineEdit" name="downloadsDirTextBox"/>
|
||||||
@ -585,7 +599,7 @@
|
|||||||
</sizepolicy>
|
</sizepolicy>
|
||||||
</property>
|
</property>
|
||||||
<property name="horizontalScrollBarPolicy">
|
<property name="horizontalScrollBarPolicy">
|
||||||
<enum>Qt::ScrollBarAlwaysOff</enum>
|
<enum>Qt::ScrollBarAsNeeded</enum>
|
||||||
</property>
|
</property>
|
||||||
<property name="undoRedoEnabled">
|
<property name="undoRedoEnabled">
|
||||||
<bool>false</bool>
|
<bool>false</bool>
|
||||||
@ -637,15 +651,33 @@
|
|||||||
</customwidgets>
|
</customwidgets>
|
||||||
<tabstops>
|
<tabstops>
|
||||||
<tabstop>tabWidget</tabstop>
|
<tabstop>tabWidget</tabstop>
|
||||||
|
<tabstop>scrollArea</tabstop>
|
||||||
<tabstop>autoUpdateCheckBox</tabstop>
|
<tabstop>autoUpdateCheckBox</tabstop>
|
||||||
|
<tabstop>updateIntervalSpinBox</tabstop>
|
||||||
<tabstop>instDirTextBox</tabstop>
|
<tabstop>instDirTextBox</tabstop>
|
||||||
<tabstop>instDirBrowseBtn</tabstop>
|
<tabstop>instDirBrowseBtn</tabstop>
|
||||||
<tabstop>modsDirTextBox</tabstop>
|
<tabstop>modsDirTextBox</tabstop>
|
||||||
<tabstop>modsDirBrowseBtn</tabstop>
|
<tabstop>modsDirBrowseBtn</tabstop>
|
||||||
<tabstop>iconsDirTextBox</tabstop>
|
<tabstop>iconsDirTextBox</tabstop>
|
||||||
<tabstop>iconsDirBrowseBtn</tabstop>
|
<tabstop>iconsDirBrowseBtn</tabstop>
|
||||||
|
<tabstop>javaDirTextBox</tabstop>
|
||||||
|
<tabstop>javaDirBrowseBtn</tabstop>
|
||||||
|
<tabstop>skinsDirTextBox</tabstop>
|
||||||
|
<tabstop>skinsDirBrowseBtn</tabstop>
|
||||||
|
<tabstop>downloadsDirTextBox</tabstop>
|
||||||
|
<tabstop>downloadsDirBrowseBtn</tabstop>
|
||||||
|
<tabstop>downloadsDirWatchRecursiveCheckBox</tabstop>
|
||||||
|
<tabstop>metadataDisableBtn</tabstop>
|
||||||
|
<tabstop>dependenciesDisableBtn</tabstop>
|
||||||
|
<tabstop>skipModpackUpdatePromptBtn</tabstop>
|
||||||
|
<tabstop>numberOfConcurrentTasksSpinBox</tabstop>
|
||||||
|
<tabstop>numberOfConcurrentDownloadsSpinBox</tabstop>
|
||||||
|
<tabstop>numberOfManualRetriesSpinBox</tabstop>
|
||||||
|
<tabstop>timeoutSecondsSpinBox</tabstop>
|
||||||
<tabstop>sortLastLaunchedBtn</tabstop>
|
<tabstop>sortLastLaunchedBtn</tabstop>
|
||||||
<tabstop>sortByNameBtn</tabstop>
|
<tabstop>sortByNameBtn</tabstop>
|
||||||
|
<tabstop>catOpacitySpinBox</tabstop>
|
||||||
|
<tabstop>preferMenuBarCheckBox</tabstop>
|
||||||
<tabstop>lineLimitSpinBox</tabstop>
|
<tabstop>lineLimitSpinBox</tabstop>
|
||||||
<tabstop>checkStopLogging</tabstop>
|
<tabstop>checkStopLogging</tabstop>
|
||||||
<tabstop>consoleFont</tabstop>
|
<tabstop>consoleFont</tabstop>
|
||||||
|
@ -60,7 +60,7 @@
|
|||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
<property name="dragDropMode">
|
<property name="dragDropMode">
|
||||||
<enum>QAbstractItemView::NoDragDrop</enum>
|
<enum>QAbstractItemView::DropOnly</enum>
|
||||||
</property>
|
</property>
|
||||||
<property name="uniformRowHeights">
|
<property name="uniformRowHeights">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
|
@ -252,8 +252,11 @@ void VersionPage::updateButtons(int row)
|
|||||||
bool VersionPage::reloadPackProfile()
|
bool VersionPage::reloadPackProfile()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
m_profile->reload(Net::Mode::Online);
|
auto result = m_profile->reload(Net::Mode::Online);
|
||||||
return true;
|
if (!result) {
|
||||||
|
QMessageBox::critical(this, tr("Error"), result.error);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
} catch (const Exception& e) {
|
} catch (const Exception& e) {
|
||||||
QMessageBox::critical(this, tr("Error"), e.cause());
|
QMessageBox::critical(this, tr("Error"), e.cause());
|
||||||
return false;
|
return false;
|
||||||
|
@ -13,6 +13,11 @@
|
|||||||
<layout class="QVBoxLayout" name="verticalLayout">
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="label">
|
<widget class="QLabel" name="label">
|
||||||
|
<property name="font">
|
||||||
|
<font>
|
||||||
|
<italic>true</italic>
|
||||||
|
</font>
|
||||||
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Note: If your FTB instances are not in the default location, select it using the button next to search.</string>
|
<string>Note: If your FTB instances are not in the default location, select it using the button next to search.</string>
|
||||||
</property>
|
</property>
|
||||||
|
@ -36,6 +36,9 @@
|
|||||||
|
|
||||||
ThemeManager::ThemeManager()
|
ThemeManager::ThemeManager()
|
||||||
{
|
{
|
||||||
|
QIcon::setFallbackThemeName(QIcon::themeName());
|
||||||
|
QIcon::setThemeSearchPaths(QIcon::themeSearchPaths() << m_iconThemeFolder.path());
|
||||||
|
|
||||||
themeDebugLog() << "Determining System Widget Theme...";
|
themeDebugLog() << "Determining System Widget Theme...";
|
||||||
const auto& style = QApplication::style();
|
const auto& style = QApplication::style();
|
||||||
m_defaultStyle = style->objectName();
|
m_defaultStyle = style->objectName();
|
||||||
@ -93,10 +96,6 @@ void ThemeManager::initializeIcons()
|
|||||||
// set icon theme search path!
|
// set icon theme search path!
|
||||||
themeDebugLog() << "<> Initializing Icon Themes";
|
themeDebugLog() << "<> Initializing Icon Themes";
|
||||||
|
|
||||||
auto searchPaths = QIcon::themeSearchPaths();
|
|
||||||
searchPaths.append(m_iconThemeFolder.path());
|
|
||||||
QIcon::setThemeSearchPaths(searchPaths);
|
|
||||||
|
|
||||||
for (const QString& id : builtinIcons) {
|
for (const QString& id : builtinIcons) {
|
||||||
IconTheme theme(id, QString(":/icons/%1").arg(id));
|
IconTheme theme(id, QString(":/icons/%1").arg(id));
|
||||||
if (!theme.load()) {
|
if (!theme.load()) {
|
||||||
@ -348,4 +347,4 @@ void ThemeManager::refresh()
|
|||||||
|
|
||||||
initializeThemes();
|
initializeThemes();
|
||||||
initializeCatPacks();
|
initializeCatPacks();
|
||||||
}
|
}
|
||||||
|
@ -1,40 +0,0 @@
|
|||||||
#include "DropLabel.h"
|
|
||||||
|
|
||||||
#include <QDropEvent>
|
|
||||||
#include <QMimeData>
|
|
||||||
|
|
||||||
DropLabel::DropLabel(QWidget* parent) : QLabel(parent)
|
|
||||||
{
|
|
||||||
setAcceptDrops(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DropLabel::dragEnterEvent(QDragEnterEvent* event)
|
|
||||||
{
|
|
||||||
event->acceptProposedAction();
|
|
||||||
}
|
|
||||||
|
|
||||||
void DropLabel::dragMoveEvent(QDragMoveEvent* event)
|
|
||||||
{
|
|
||||||
event->acceptProposedAction();
|
|
||||||
}
|
|
||||||
|
|
||||||
void DropLabel::dragLeaveEvent(QDragLeaveEvent* event)
|
|
||||||
{
|
|
||||||
event->accept();
|
|
||||||
}
|
|
||||||
|
|
||||||
void DropLabel::dropEvent(QDropEvent* event)
|
|
||||||
{
|
|
||||||
const QMimeData* mimeData = event->mimeData();
|
|
||||||
|
|
||||||
if (!mimeData) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mimeData->hasUrls()) {
|
|
||||||
auto urls = mimeData->urls();
|
|
||||||
emit droppedURLs(urls);
|
|
||||||
}
|
|
||||||
|
|
||||||
event->acceptProposedAction();
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QLabel>
|
|
||||||
|
|
||||||
class DropLabel : public QLabel {
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit DropLabel(QWidget* parent = nullptr);
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void droppedURLs(QList<QUrl> urls);
|
|
||||||
|
|
||||||
protected:
|
|
||||||
void dropEvent(QDropEvent* event) override;
|
|
||||||
void dragEnterEvent(QDragEnterEvent* event) override;
|
|
||||||
void dragMoveEvent(QDragMoveEvent* event) override;
|
|
||||||
void dragLeaveEvent(QDragLeaveEvent* event) override;
|
|
||||||
};
|
|
@ -279,16 +279,17 @@ void ModFilterWidget::onSideFilterChanged()
|
|||||||
{
|
{
|
||||||
QString side;
|
QString side;
|
||||||
|
|
||||||
if (ui->clientSide->isChecked() != ui->serverSide->isChecked()) {
|
if (ui->clientSide->isChecked() && !ui->serverSide->isChecked()) {
|
||||||
if (ui->clientSide->isChecked())
|
side = "client";
|
||||||
side = "client";
|
} else if (!ui->clientSide->isChecked() && ui->serverSide->isChecked()) {
|
||||||
else
|
side = "server";
|
||||||
side = "server";
|
} else if (ui->clientSide->isChecked() && ui->serverSide->isChecked()) {
|
||||||
|
side = "both";
|
||||||
} else {
|
} else {
|
||||||
// both are checked or none are checked; in either case no filtering will happen
|
|
||||||
side = "";
|
side = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
m_filter_changed = side != m_filter->side;
|
m_filter_changed = side != m_filter->side;
|
||||||
m_filter->side = side;
|
m_filter->side = side;
|
||||||
if (m_filter_changed)
|
if (m_filter_changed)
|
||||||
|
@ -47,6 +47,9 @@
|
|||||||
<property name="wordWrap">
|
<property name="wordWrap">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="textInteractionFlags">
|
||||||
|
<set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
|
||||||
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
@ -68,6 +71,9 @@
|
|||||||
<property name="alignment">
|
<property name="alignment">
|
||||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="textInteractionFlags">
|
||||||
|
<set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
|
||||||
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
|
12
program_info/AdhocSignedApp.entitlements
Normal file
12
program_info/AdhocSignedApp.entitlements
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.cs.disable-library-validation</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.device.audio-input</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.device.camera</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
@ -2,10 +2,6 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>com.apple.security.cs.disable-library-validation</key>
|
|
||||||
<true/>
|
|
||||||
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
|
|
||||||
<true/>
|
|
||||||
<key>com.apple.security.device.audio-input</key>
|
<key>com.apple.security.device.audio-input</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.device.camera</key>
|
<key>com.apple.security.device.camera</key>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user