Merge branch 'develop' into desysteminfo

Signed-off-by: TheKodeToad <TheKodeToad@proton.me>
This commit is contained in:
TheKodeToad 2025-11-23 16:23:27 +00:00 committed by GitHub
commit 19aab36a4d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
121 changed files with 1479 additions and 1897 deletions

View file

@ -13,10 +13,6 @@ inputs:
description: Name of the uploaded artifact
required: true
default: Linux
cmake-preset:
description: Base CMake preset previously used for the build
required: true
default: linux
qt-version:
description: Version of Qt to use
required: true
@ -62,7 +58,7 @@ runs:
GPG_PRIVATE_KEY: ${{ inputs.gpg-private-key }}
run: |
cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_APPIMAGE_DIR }}/usr
cmake --install ${{ env.BUILD_DIR }} --config ${{ inputs.build-type }} --prefix ${{ env.INSTALL_APPIMAGE_DIR }}/usr
mv ${{ env.INSTALL_APPIMAGE_DIR }}/usr/share/metainfo/org.prismlauncher.PrismLauncher.metainfo.xml ${{ env.INSTALL_APPIMAGE_DIR }}/usr/share/metainfo/org.prismlauncher.PrismLauncher.appdata.xml
export "NO_APPSTREAM=1" # we have to skip appstream checking because appstream on ubuntu 20.04 is outdated
@ -107,12 +103,10 @@ runs:
env:
BUILD_DIR: build
CMAKE_PRESET: ${{ inputs.cmake-preset }}
INSTALL_PORTABLE_DIR: install-portable
run: |
cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }}
cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable
cmake --install ${{ env.BUILD_DIR }} --config ${{ inputs.build-type }} --prefix ${{ env.INSTALL_PORTABLE_DIR }}
cmake --install ${{ env.BUILD_DIR }} --config ${{ inputs.build-type }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable
for l in $(find ${{ env.INSTALL_PORTABLE_DIR }} -type f); do l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_PORTABLE_DIR }}/}; l=${l#./}; echo $l; done > ${{ env.INSTALL_PORTABLE_DIR }}/manifest.txt
cd ${{ env.INSTALL_PORTABLE_DIR }}

View file

@ -59,7 +59,7 @@ runs:
BUILD_DIR: build
INSTALL_DIR: install
run: |
cmake --install ${{ env.BUILD_DIR }}
cmake --install ${{ env.BUILD_DIR }} --config ${{ inputs.build-type }}
cd ${{ env.INSTALL_DIR }}
chmod +x "PrismLauncher.app/Contents/MacOS/prismlauncher"

View file

@ -15,12 +15,15 @@ inputs:
msystem:
description: MSYS2 subsystem to use
required: false
windows-codesign-cert:
description: Certificate for signing Windows builds
required: false
windows-codesign-password:
description: Password for signing Windows builds
required: false
azure-client-id:
description: Client ID for the Azure Signer Application
required: true
azure-tenant-id:
description: Tenant ID for the Azure Signer Application
required: true
azure-subscription-id:
description: Subscription ID for the Azure Signer Application
required: true
runs:
using: composite
@ -33,7 +36,7 @@ runs:
BUILD_DIR: build
INSTALL_DIR: install
run: |
cmake --install ${{ env.BUILD_DIR }}
cmake --install ${{ env.BUILD_DIR }} --config ${{ inputs.build-type }}
touch ${{ env.INSTALL_DIR }}/manifest.txt
for l in $(find ${{ env.INSTALL_DIR }} -type f); do l=$(cygpath -u $l); l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_DIR }}/}; l=${l#./}; echo $l; done >> ${{ env.INSTALL_DIR }}/manifest.txt
@ -50,23 +53,45 @@ runs:
Get-ChildItem ${{ env.INSTALL_DIR }} -Recurse | ForEach FullName | Resolve-Path -Relative | %{ $_.TrimStart('.\') } | %{ $_.TrimStart('${{ env.INSTALL_DIR }}') } | %{ $_.TrimStart('\') } | Out-File -FilePath ${{ env.INSTALL_DIR }}/manifest.txt
- name: Fetch codesign certificate
shell: bash # yes, we are not using MSYS2 or PowerShell here
run: |
echo '${{ inputs.windows-codesign-cert }}' | base64 --decode > codesign.pfx
- name: Sign executable
- name: Emit warning for unsigned builds
if: ${{ github.ref_name != 'develop' || inputs.azure-client-id == '' }}
shell: pwsh
env:
INSTALL_DIR: install
run: |
if (Get-Content ./codesign.pfx){
cd ${{ env.INSTALL_DIR }}
# We ship the exact same executable for portable and non-portable editions, so signing just once is fine
SignTool sign /fd sha256 /td sha256 /f ../codesign.pfx /p '${{ inputs.windows-codesign-password }}' /tr http://timestamp.digicert.com prismlauncher.exe prismlauncher_updater.exe prismlauncher_filelink.exe
} else {
":warning: Skipped code signing for Windows, as certificate was not present." >> $env:GITHUB_STEP_SUMMARY
}
":warning: Skipped code signing for Windows, as certificate was not present." >> $env:GITHUB_STEP_SUMMARY
- name: Login to Azure
if: ${{ github.ref_name == 'develop' && inputs.azure-client-id != '' }}
uses: azure/login@v2
with:
client-id: ${{ inputs.azure-client-id }}
tenant-id: ${{ inputs.azure-tenant-id }}
subscription-id: ${{ inputs.azure-subscription-id }}
- name: Sign executables
if: ${{ github.ref_name == 'develop' && inputs.azure-client-id != '' }}
uses: azure/trusted-signing-action@v0
with:
endpoint: https://eus.codesigning.azure.net/
trusted-signing-account-name: PrismLauncher
certificate-profile-name: PrismLauncher
files: |
${{ github.workspace }}\install\prismlauncher.exe
${{ github.workspace }}\install\prismlauncher_filelink.exe
${{ github.workspace }}\install\prismlauncher_updater.exe
# TODO(@getchoo): Is this all really needed???
# https://github.com/Azure/trusted-signing-action/blob/fc390cf8ed0f14e248a542af1d838388a47c7a7c/docs/OIDC.md
exclude-environment-credential: true
exclude-workload-identity-credential: true
exclude-managed-identity-credential: true
exclude-shared-token-cache-credential: true
exclude-visual-studio-credential: true
exclude-visual-studio-code-credential: true
exclude-azure-cli-credential: false
exclude-azure-powershell-credential: true
exclude-azure-developer-cli-credential: true
exclude-interactive-browser-credential: true
- name: Package (MinGW, portable)
if: ${{ inputs.msystem != '' }}
@ -77,7 +102,7 @@ runs:
INSTALL_PORTABLE_DIR: install-portable
run: |
cp -r ${{ env.INSTALL_DIR }} ${{ env.INSTALL_PORTABLE_DIR }} # cmake install on Windows is slow, let's just copy instead
cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable
cmake --install ${{ env.BUILD_DIR }} --config ${{ inputs.build-type }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable
for l in $(find ${{ env.INSTALL_PORTABLE_DIR }} -type f); do l=$(cygpath -u $l); l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_PORTABLE_DIR }}/}; l=${l#./}; echo $l; done >> ${{ env.INSTALL_PORTABLE_DIR }}/manifest.txt
- name: Package (MSVC, portable)
@ -89,7 +114,7 @@ runs:
INSTALL_PORTABLE_DIR: install-portable
run: |
cp -r ${{ env.INSTALL_DIR }} ${{ env.INSTALL_PORTABLE_DIR }} # cmake install on Windows is slow, let's just copy instead
cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable
cmake --install ${{ env.BUILD_DIR }} --config ${{ inputs.build-type }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable
Get-ChildItem ${{ env.INSTALL_PORTABLE_DIR }} -Recurse | ForEach FullName | Resolve-Path -Relative | %{ $_.TrimStart('.\') } | %{ $_.TrimStart('${{ env.INSTALL_PORTABLE_DIR }}') } | %{ $_.TrimStart('\') } | Out-File -FilePath ${{ env.INSTALL_DIR }}/manifest.txt
@ -115,13 +140,28 @@ runs:
makensis -NOCD "${{ github.workspace }}/${{ env.BUILD_DIR }}/program_info/win_install.nsi"
- name: Sign installer
shell: pwsh
run: |
if (Get-Content ./codesign.pfx){
SignTool sign /fd sha256 /td sha256 /f codesign.pfx /p '${{ inputs.windows-codesign-password }}' /tr http://timestamp.digicert.com PrismLauncher-Setup.exe
} else {
":warning: Skipped code signing for Windows, as certificate was not present." >> $env:GITHUB_STEP_SUMMARY
}
if: ${{ github.ref_name == 'develop' && inputs.azure-client-id != '' }}
uses: azure/trusted-signing-action@v0
with:
endpoint: https://eus.codesigning.azure.net/
trusted-signing-account-name: PrismLauncher
certificate-profile-name: PrismLauncher
files: |
${{ github.workspace }}\PrismLauncher-Setup.exe
# TODO(@getchoo): Is this all really needed???
# https://github.com/Azure/trusted-signing-action/blob/fc390cf8ed0f14e248a542af1d838388a47c7a7c/docs/OIDC.md
exclude-environment-credential: true
exclude-workload-identity-credential: true
exclude-managed-identity-credential: true
exclude-shared-token-cache-credential: true
exclude-visual-studio-credential: true
exclude-visual-studio-code-credential: true
exclude-azure-cli-credential: false
exclude-azure-powershell-credential: true
exclude-azure-developer-cli-credential: true
exclude-interactive-browser-credential: true
- name: Upload binary zip
uses: actions/upload-artifact@v5

View file

@ -11,6 +11,7 @@ runs:
sudo apt-get -y install \
dpkg-dev \
ninja-build extra-cmake-modules scdoc \
libqrencode-dev \
appstream libxcb-cursor-dev
- name: Setup AppImage tooling

View file

@ -79,6 +79,7 @@ runs:
qt6-5compat:p
qt6-networkauth:p
cmark:p
qrencode:p
tomlplusplus:p
quazip-qt6:p

View file

@ -70,6 +70,8 @@ jobs:
name: Build (${{ matrix.artifact-name }})
permissions:
# Required for Azure Trusted Signing
id-token: write
# Required for vcpkg binary cache
packages: write
@ -79,7 +81,7 @@ jobs:
include:
- os: ubuntu-22.04
artifact-name: Linux
base-cmake-preset: linux
cmake-preset: linux
# NOTE(@getchoo): Yes, we're intentionally using 24.04 here!!!
#
@ -87,34 +89,34 @@ jobs:
# *for the same version* are compiled against 24.04 on ARM, and *not* 22.04 like x64
- os: ubuntu-24.04-arm
artifact-name: Linux-aarch64
base-cmake-preset: linux
cmake-preset: linux
- os: windows-2022
artifact-name: Windows-MinGW-w64
base-cmake-preset: windows_mingw
cmake-preset: windows_mingw
msystem: CLANG64
vcvars-arch: amd64_x86
- os: windows-11-arm
artifact-name: Windows-MinGW-arm64
base-cmake-preset: windows_mingw
cmake-preset: windows_mingw
msystem: CLANGARM64
vcvars-arch: arm64
- os: windows-2022
artifact-name: Windows-MSVC
base-cmake-preset: windows_msvc
cmake-preset: windows_msvc
# TODO(@getchoo): This is the default in setup-dependencies/windows. Why isn't it working?!?!
vcvars-arch: amd64
- os: windows-11-arm
artifact-name: Windows-MSVC-arm64
base-cmake-preset: windows_msvc
cmake-preset: windows_msvc
vcvars-arch: arm64
- os: macos-14
artifact-name: macOS
base-cmake-preset: ${{ (inputs.build-type || 'Debug') == 'Debug' && 'macos_universal' || 'macos' }}
cmake-preset: macos_universal
macosx-deployment-target: 12.0
runs-on: ${{ matrix.os }}
@ -124,6 +126,11 @@ jobs:
shell: ${{ matrix.msystem != '' && 'msys2 {0}' || 'bash' }}
env:
ARTIFACT_NAME: ${{ matrix.artifact-name }}-Qt6
BUILD_PLATFORM: official
BUILD_TYPE: ${{ inputs.build-type || 'Debug' }}
CMAKE_PRESET: ${{ matrix.cmake-preset }}
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx-deployment-target }}
steps:
@ -140,7 +147,7 @@ jobs:
id: setup-dependencies
uses: ./.github/actions/setup-dependencies
with:
build-type: ${{ inputs.build-type || 'Debug' }}
build-type: ${{ env.BUILD_TYPE }}
artifact-name: ${{ matrix.artifact-name }}
msystem: ${{ matrix.msystem }}
vcvars-arch: ${{ matrix.vcvars-arch }}
@ -150,22 +157,17 @@ jobs:
# BUILD
##
- name: Get CMake preset
id: cmake-preset
env:
BASE_CMAKE_PRESET: ${{ matrix.base-cmake-preset }}
PRESET_TYPE: ${{ (inputs.build-type || 'Debug') == 'Debug' && 'debug' || 'ci' }}
- name: Configure project
run: |
echo preset="$BASE_CMAKE_PRESET"_"$PRESET_TYPE" >> "$GITHUB_OUTPUT"
cmake --preset "$CMAKE_PRESET"
- name: Run CMake workflow
env:
CMAKE_PRESET: ${{ steps.cmake-preset.outputs.preset }}
ARTIFACT_NAME: ${{ matrix.artifact-name }}-Qt6
BUILD_PLATFORM: official
- name: Run build
run: |
cmake --workflow --preset "$CMAKE_PRESET"
cmake --build --preset "$CMAKE_PRESET" --config "$BUILD_TYPE"
- name: Run tests
run: |
ctest --preset "$CMAKE_PRESET" --build-config "$BUILD_TYPE"
##
# PACKAGE
@ -184,7 +186,6 @@ jobs:
version: ${{ steps.short-version.outputs.version }}
build-type: ${{ steps.setup-dependencies.outputs.build-type }}
artifact-name: ${{ matrix.artifact-name }}
cmake-preset: ${{ steps.cmake-preset.outputs.preset }}
qt-version: ${{ steps.setup-dependencies.outputs.qt-version }}
gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }}
@ -198,13 +199,13 @@ jobs:
build-type: ${{ steps.setup-dependencies.outputs.build-type }}
artifact-name: ${{ matrix.artifact-name }}
apple-codesign-cert: ${{ secrets.APPLE-CODESIGN-CERT }}
apple-codesign-password: ${{ secrets.APPLE-CODESIGN_PASSWORD }}
apple-codesign-id: ${{ secrets.APPLE-CODESIGN_ID }}
apple-codesign-cert: ${{ secrets.APPLE_CODESIGN_CERT }}
apple-codesign-password: ${{ secrets.APPLE_CODESIGN_PASSWORD }}
apple-codesign-id: ${{ secrets.APPLE_CODESIGN_ID }}
apple-notarize-apple-id: ${{ secrets.APPLE_NOTARIZE_APPLE_ID }}
apple-notarize-team-id: ${{ secrets.APPLE_NOTARIZE_TEAM_ID }}
apple-notarize-password: ${{ secrets.APPLE-NOTARIZE_PASSWORD }}
sparkle-ed25519-key: ${{ secrets.SPARKLE-ED25519_KEY }}
apple-notarize-password: ${{ secrets.APPLE_NOTARIZE_PASSWORD }}
sparkle-ed25519-key: ${{ secrets.SPARKLE_ED25519_KEY }}
- name: Package (Windows)
if: ${{ runner.os == 'Windows' }}
@ -215,5 +216,6 @@ jobs:
artifact-name: ${{ matrix.artifact-name }}
msystem: ${{ matrix.msystem }}
windows-codesign-cert: ${{ secrets.WINDOWS_CODESIGN_CERT }}
windows-codesign-password: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }}
azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

View file

@ -78,8 +78,8 @@ jobs:
- name: Configure and Build
run: |
cmake --preset linux_debug
cmake --build --preset linux_debug
cmake --preset linux
cmake --build --preset linux --config Debug
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4

View file

@ -17,7 +17,7 @@ jobs:
steps:
- uses: actions/checkout@v5
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
- uses: DeterminateSystems/update-flake-lock@v27
with:

3
.gitmodules vendored
View file

@ -19,6 +19,3 @@
[submodule "flatpak/shared-modules"]
path = flatpak/shared-modules
url = https://github.com/flathub/shared-modules.git
[submodule "libraries/qrcodegenerator"]
path = libraries/qrcodegenerator
url = https://github.com/nayuki/QR-Code-generator

View file

@ -112,7 +112,7 @@ set(CMAKE_CXX_FLAGS_RELEASE "-O2 -D_FORTIFY_SOURCE=2 ${CMAKE_CXX_FLAGS_RELEASE}"
# Export compile commands for debug builds if we can (useful in LSPs like clangd)
# https://cmake.org/cmake/help/v3.31/variable/CMAKE_EXPORT_COMPILE_COMMANDS.html
if(CMAKE_GENERATOR STREQUAL "Unix Makefiles" OR CMAKE_GENERATOR STREQUAL "Ninja" AND CMAKE_BUILD_TYPE STREQUAL "Debug")
if(CMAKE_GENERATOR STREQUAL "Unix Makefiles" OR CMAKE_GENERATOR MATCHES "^Ninja")
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
endif()
@ -345,12 +345,24 @@ if(Launcher_QT_VERSION_MAJOR EQUAL 6)
set(QT_LIBEXECS_DIR ${QT${QT_VERSION_MAJOR}_INSTALL_PREFIX}/${QT${QT_VERSION_MAJOR}_INSTALL_LIBEXECS})
endif()
# Find libqrencode
## NOTE(@getchoo): Never use pkg-config with MSVC since the vcpkg port makes our install bundle fail to find the dll
if(MSVC)
find_path(LIBQRENCODE_INCLUDE_DIR qrencode.h REQUIRED)
find_library(LIBQRENCODE_LIBRARY_RELEASE qrencode REQUIRED)
find_library(LIBQRENCODE_LIBRARY_DEBUG qrencoded)
set(LIBQRENCODE_LIBRARIES optimized ${LIBQRENCODE_LIBRARY_RELEASE} debug ${LIBQRENCODE_LIBRARY_DEBUG})
else()
find_package(PkgConfig REQUIRED)
pkg_check_modules(libqrencode REQUIRED IMPORTED_TARGET libqrencode)
endif()
if(NOT Launcher_FORCE_BUNDLED_LIBS)
# Find toml++
find_package(tomlplusplus 3.2.0 QUIET)
# Fallback to pkg-config (if available) if CMake files aren't found
if(NOT tomlplusplus_FOUND)
find_package(PkgConfig)
find_package(PkgConfig QUIET)
if(PkgConfig_FOUND)
pkg_check_modules(tomlplusplus IMPORTED_TARGET tomlplusplus>=3.2.0)
endif()
@ -359,9 +371,6 @@ if(NOT Launcher_FORCE_BUNDLED_LIBS)
# Find cmark
find_package(cmark QUIET)
# Find qrcodegencpp-cmake
find_package(qrcodegencpp QUIET)
endif()
include(ECMQtDeclareLoggingCategory)
@ -405,8 +414,8 @@ if(UNIX AND APPLE)
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_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 "50612a06038abc931f16011d7903b8326a362c1074dabccb718404ce8e585f0b" CACHE STRING "SHA256 checksum for Sparkle release archive")
set(MACOSX_SPARKLE_DOWNLOAD_URL "https://github.com/sparkle-project/Sparkle/releases/download/2.8.0/Sparkle-2.8.0.tar.xz" CACHE STRING "URL to Sparkle release archive")
set(MACOSX_SPARKLE_SHA256 "fd5681ee92bf238aaac2d08214ceaf0cc8976e452d7f882d80bac1e61581f3b1" CACHE STRING "SHA256 checksum for Sparkle release archive")
set(MACOSX_SPARKLE_DIR "${CMAKE_BINARY_DIR}/frameworks/Sparkle")
if(NOT MACOSX_SPARKLE_UPDATE_PUBLIC_KEY STREQUAL "" AND NOT MACOSX_SPARKLE_UPDATE_FEED_URL STREQUAL "")
@ -522,18 +531,6 @@ if(NOT cmark_FOUND)
else()
message(STATUS "Using system cmark")
endif()
if(NOT qrcodegencpp_FOUND)
set(QRCODE_SOURCES
libraries/qrcodegenerator/cpp/qrcodegen.cpp
libraries/qrcodegenerator/cpp/qrcodegen.hpp
)
add_library(qrcodegenerator STATIC ${QRCODE_SOURCES})
target_include_directories(qrcodegenerator PUBLIC "libraries/qrcodegenerator/cpp/" )
generate_export_header(qrcodegenerator)
else()
add_library(qrcodegenerator ALIAS qrcodegencpp::qrcodegencpp)
message(STATUS "Using system qrcodegencpp-cmake")
endif()
add_subdirectory(libraries/gamemode)
add_subdirectory(libraries/murmur2) # Hash for usage with the CurseForge API
add_subdirectory(libraries/qdcss) # css parser

View file

@ -5,10 +5,210 @@
"major": 3,
"minor": 28
},
"include": [
"cmake/linuxPreset.json",
"cmake/macosPreset.json",
"cmake/windowsMinGWPreset.json",
"cmake/windowsMSVCPreset.json"
"configurePresets": [
{
"name": "base",
"hidden": true,
"binaryDir": "build",
"installDir": "install",
"generator": "Ninja Multi-Config",
"cacheVariables": {
"Launcher_BUILD_ARTIFACT": "$penv{ARTIFACT_NAME}",
"Launcher_BUILD_PLATFORM": "$penv{BUILD_PLATFORM}",
"Launcher_ENABLE_JAVA_DOWNLOADER": "ON",
"ENABLE_LTO": "ON"
}
},
{
"name": "linux",
"displayName": "Linux",
"inherits": [
"base"
],
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Linux"
}
},
{
"name": "macos",
"displayName": "macOS",
"inherits": [
"base"
],
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Darwin"
}
},
{
"name": "macos_universal",
"displayName": "macOS (Universal Binary)",
"inherits": [
"macos"
],
"cacheVariables": {
"CMAKE_OSX_ARCHITECTURES": "x86_64;arm64",
"VCPKG_TARGET_TRIPLET": "universal-osx"
}
},
{
"name": "windows_mingw",
"displayName": "Windows (MinGW)",
"inherits": [
"base"
],
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Windows"
}
},
{
"name": "windows_msvc",
"displayName": "Windows (MSVC)",
"inherits": [
"base"
],
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Windows"
}
}
],
"buildPresets": [
{
"name": "linux",
"displayName": "Linux",
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Linux"
},
"configurePreset": "linux"
},
{
"name": "macos",
"displayName": "macOS",
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Darwin"
},
"configurePreset": "macos"
},
{
"name": "macos_universal",
"displayName": "macOS (Universal Binary)",
"inherits": [
"macos"
],
"configurePreset": "macos_universal"
},
{
"name": "windows_mingw",
"displayName": "Windows (MinGW)",
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Windows"
},
"configurePreset": "windows_mingw"
},
{
"name": "windows_msvc",
"displayName": "Windows (MSVC)",
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Windows"
},
"configurePreset": "windows_msvc"
}
],
"testPresets": [
{
"name": "base",
"hidden": true,
"output": {
"outputOnFailure": true
},
"execution": {
"noTestsAction": "error"
},
"filter": {
"exclude": {
"name": "^example64|example$"
}
}
},
{
"name": "linux",
"displayName": "Linux",
"inherits": [
"base"
],
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Linux"
},
"configurePreset": "linux"
},
{
"name": "macos",
"displayName": "macOS",
"inherits": [
"base"
],
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Darwin"
},
"configurePreset": "macos"
},
{
"name": "macos_universal",
"displayName": "macOS (Universal Binary)",
"inherits": [
"base"
],
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Darwin"
},
"configurePreset": "macos_universal"
},
{
"name": "windows_mingw",
"displayName": "Windows (MinGW)",
"inherits": [
"base"
],
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Windows"
},
"configurePreset": "windows_mingw"
},
{
"name": "windows_msvc",
"displayName": "Windows (MSVC)",
"inherits": [
"base"
],
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Windows"
},
"configurePreset": "windows_msvc"
}
]
}

View file

@ -404,14 +404,23 @@
You should have received a copy of the GNU Lesser General Public
License along with this library. If not, see <http://www.gnu.org/licenses/>.
## QR-Code-generator (`libraries/qrcodegenerator`)
## libqrencode (`fukuchi/libqrencode`)
Copyright © 2024 Project Nayuki. (MIT License)
https://www.nayuki.io/page/qr-code-generator-library
Copyright (C) 2020 libqrencode Authors
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
- The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose and noninfringement. In no event shall the authors or copyright holders be liable for any claim, damages or other liability, whether in an action of contract, tort or otherwise, arising from, out of or in connection with the Software or the use or other dealings in the Software.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
## vcpkg (`cmake/vcpkg-ports`)

View file

@ -1,81 +0,0 @@
{
"$schema": "https://cmake.org/cmake/help/latest/_downloads/3e2d73bff478d88a7de0de736ba5e361/schema.json",
"version": 8,
"configurePresets": [
{
"name": "base",
"hidden": true,
"binaryDir": "build",
"installDir": "install",
"cacheVariables": {
"Launcher_BUILD_ARTIFACT": "$penv{ARTIFACT_NAME}",
"Launcher_BUILD_PLATFORM": "$penv{BUILD_PLATFORM}"
}
},
{
"name": "base_debug",
"hidden": true,
"inherits": [
"base"
],
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug"
}
},
{
"name": "base_release",
"hidden": true,
"inherits": [
"base"
],
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"ENABLE_LTO": "ON"
}
},
{
"name": "base_ci",
"hidden": true,
"inherits": [
"base_release"
],
"cacheVariables": {
"Launcher_FORCE_BUNDLED_LIBS": "ON"
}
}
],
"testPresets": [
{
"name": "base",
"hidden": true,
"output": {
"outputOnFailure": true
},
"execution": {
"noTestsAction": "error"
},
"filter": {
"exclude": {
"name": "^example64|example$"
}
}
},
{
"name": "base_debug",
"hidden": true,
"inherits": [
"base"
],
"output": {
"debug": true
}
},
{
"name": "base_release",
"hidden": true,
"inherits": [
"base"
]
}
]
}

View file

@ -1,176 +0,0 @@
{
"$schema": "https://cmake.org/cmake/help/latest/_downloads/3e2d73bff478d88a7de0de736ba5e361/schema.json",
"version": 8,
"include": [
"commonPresets.json"
],
"configurePresets": [
{
"name": "linux_base",
"hidden": true,
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Linux"
},
"generator": "Ninja",
"cacheVariables": {
"Launcher_ENABLE_JAVA_DOWNLOADER": "ON"
}
},
{
"name": "linux_debug",
"inherits": [
"base_debug",
"linux_base"
],
"displayName": "Linux (Debug)"
},
{
"name": "linux_release",
"inherits": [
"base_release",
"linux_base"
],
"displayName": "Linux (Release)"
},
{
"name": "linux_ci",
"inherits": [
"base_ci",
"linux_base"
],
"displayName": "Linux (CI)",
"installDir": "/usr"
}
],
"buildPresets": [
{
"name": "linux_base",
"hidden": true,
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Linux"
}
},
{
"name": "linux_debug",
"inherits": [
"linux_base"
],
"displayName": "Linux (Debug)",
"configurePreset": "linux_debug"
},
{
"name": "linux_release",
"inherits": [
"linux_base"
],
"displayName": "Linux (Release)",
"configurePreset": "linux_release"
},
{
"name": "linux_ci",
"inherits": [
"linux_base"
],
"displayName": "Linux (CI)",
"configurePreset": "linux_ci"
}
],
"testPresets": [
{
"name": "linux_base",
"hidden": true,
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Linux"
}
},
{
"name": "linux_debug",
"inherits": [
"base_debug",
"linux_base"
],
"displayName": "Linux (Debug)",
"configurePreset": "linux_debug"
},
{
"name": "linux_release",
"inherits": [
"base_release",
"linux_base"
],
"displayName": "Linux (Release)",
"configurePreset": "linux_release"
},
{
"name": "linux_ci",
"inherits": [
"base_release",
"linux_base"
],
"displayName": "Linux (CI)",
"configurePreset": "linux_ci"
}
],
"workflowPresets": [
{
"name": "linux_debug",
"displayName": "Linux (Debug)",
"steps": [
{
"type": "configure",
"name": "linux_debug"
},
{
"type": "build",
"name": "linux_debug"
},
{
"type": "test",
"name": "linux_debug"
}
]
},
{
"name": "linux",
"displayName": "Linux (Release)",
"steps": [
{
"type": "configure",
"name": "linux_release"
},
{
"type": "build",
"name": "linux_release"
},
{
"type": "test",
"name": "linux_release"
}
]
},
{
"name": "linux_ci",
"displayName": "Linux (CI)",
"steps": [
{
"type": "configure",
"name": "linux_ci"
},
{
"type": "build",
"name": "linux_ci"
},
{
"type": "test",
"name": "linux_ci"
}
]
}
]
}

View file

@ -1,269 +0,0 @@
{
"$schema": "https://cmake.org/cmake/help/latest/_downloads/3e2d73bff478d88a7de0de736ba5e361/schema.json",
"version": 8,
"include": [
"commonPresets.json"
],
"configurePresets": [
{
"name": "macos_base",
"hidden": true,
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Darwin"
},
"generator": "Ninja"
},
{
"name": "macos_universal_base",
"hidden": true,
"inherits": [
"macos_base"
],
"cacheVariables": {
"CMAKE_OSX_ARCHITECTURES": "x86_64;arm64",
"VCPKG_TARGET_TRIPLET": "universal-osx"
}
},
{
"name": "macos_debug",
"inherits": [
"base_debug",
"macos_base"
],
"displayName": "macOS (Debug)"
},
{
"name": "macos_release",
"inherits": [
"base_release",
"macos_base"
],
"displayName": "macOS (Release)"
},
{
"name": "macos_universal_debug",
"inherits": [
"base_debug",
"macos_universal_base"
],
"displayName": "macOS (Universal Binary, Debug)"
},
{
"name": "macos_universal_release",
"inherits": [
"base_release",
"macos_universal_base"
],
"displayName": "macOS (Universal Binary, Release)"
},
{
"name": "macos_ci",
"inherits": [
"base_ci",
"macos_universal_base"
],
"displayName": "macOS (CI)"
}
],
"buildPresets": [
{
"name": "macos_base",
"hidden": true,
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Darwin"
}
},
{
"name": "macos_debug",
"inherits": [
"macos_base"
],
"displayName": "macOS (Debug)",
"configurePreset": "macos_debug"
},
{
"name": "macos_release",
"inherits": [
"macos_base"
],
"displayName": "macOS (Release)",
"configurePreset": "macos_release"
},
{
"name": "macos_universal_debug",
"inherits": [
"macos_base"
],
"displayName": "macOS (Universal Binary, Debug)",
"configurePreset": "macos_universal_debug"
},
{
"name": "macos_universal_release",
"inherits": [
"macos_base"
],
"displayName": "macOS (Universal Binary, Release)",
"configurePreset": "macos_universal_release"
},
{
"name": "macos_ci",
"inherits": [
"macos_base"
],
"displayName": "macOS (CI)",
"configurePreset": "macos_ci"
}
],
"testPresets": [
{
"name": "macos_base",
"hidden": true,
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Darwin"
}
},
{
"name": "macos_debug",
"inherits": [
"base_debug",
"macos_base"
],
"displayName": "MacOS (Debug)",
"configurePreset": "macos_debug"
},
{
"name": "macos_release",
"inherits": [
"base_release",
"macos_base"
],
"displayName": "macOS (Release)",
"configurePreset": "macos_release"
},
{
"name": "macos_universal_debug",
"inherits": [
"base_debug",
"macos_base"
],
"displayName": "MacOS (Universal Binary, Debug)",
"configurePreset": "macos_universal_debug"
},
{
"name": "macos_universal_release",
"inherits": [
"base_release",
"macos_base"
],
"displayName": "macOS (Universal Binary, Release)",
"configurePreset": "macos_universal_release"
},
{
"name": "macos_ci",
"inherits": [
"base_release",
"macos_base"
],
"displayName": "macOS (CI)",
"configurePreset": "macos_ci"
}
],
"workflowPresets": [
{
"name": "macos_debug",
"displayName": "macOS (Debug)",
"steps": [
{
"type": "configure",
"name": "macos_debug"
},
{
"type": "build",
"name": "macos_debug"
},
{
"type": "test",
"name": "macos_debug"
}
]
},
{
"name": "macos",
"displayName": "macOS (Release)",
"steps": [
{
"type": "configure",
"name": "macos_release"
},
{
"type": "build",
"name": "macos_release"
},
{
"type": "test",
"name": "macos_release"
}
]
},
{
"name": "macos_universal_debug",
"displayName": "macOS (Universal Binary, Debug)",
"steps": [
{
"type": "configure",
"name": "macos_universal_debug"
},
{
"type": "build",
"name": "macos_universal_debug"
},
{
"type": "test",
"name": "macos_universal_debug"
}
]
},
{
"name": "macos_universal",
"displayName": "macOS (Universal Binary, Release)",
"steps": [
{
"type": "configure",
"name": "macos_universal_release"
},
{
"type": "build",
"name": "macos_universal_release"
},
{
"type": "test",
"name": "macos_universal_release"
}
]
},
{
"name": "macos_ci",
"displayName": "macOS (CI)",
"steps": [
{
"type": "configure",
"name": "macos_ci"
},
{
"type": "build",
"name": "macos_ci"
},
{
"type": "test",
"name": "macos_ci"
}
]
}
]
}

View file

@ -1,281 +0,0 @@
{
"$schema": "https://cmake.org/cmake/help/latest/_downloads/3e2d73bff478d88a7de0de736ba5e361/schema.json",
"version": 8,
"include": [
"commonPresets.json"
],
"configurePresets": [
{
"name": "windows_msvc_base",
"hidden": true,
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Windows"
},
"generator": "Ninja"
},
{
"name": "windows_msvc_arm64_cross_base",
"hidden": true,
"inherits": [
"windows_msvc_base"
],
"cacheVariables": {
"CMAKE_SYSTEM_NAME": "${hostSystemName}"
}
},
{
"name": "windows_msvc_debug",
"inherits": [
"base_debug",
"windows_msvc_base"
],
"displayName": "Windows MSVC (Debug)"
},
{
"name": "windows_msvc_release",
"inherits": [
"base_release",
"windows_msvc_base"
],
"displayName": "Windows MSVC (Release)"
},
{
"name": "windows_msvc_arm64_cross_debug",
"inherits": [
"base_debug",
"windows_msvc_arm64_cross_base"
],
"displayName": "Windows MSVC (ARM64 cross, Debug)"
},
{
"name": "windows_msvc_arm64_cross_release",
"inherits": [
"base_release",
"windows_msvc_arm64_cross_base"
],
"displayName": "Windows MSVC (ARM64 cross, Release)"
},
{
"name": "windows_msvc_ci",
"inherits": [
"base_ci",
"windows_msvc_base"
],
"displayName": "Windows MSVC (CI)"
},
{
"name": "windows_msvc_arm64_cross_ci",
"inherits": [
"base_ci",
"windows_msvc_arm64_cross_base"
],
"displayName": "Windows MSVC (ARM64 cross, CI)"
}
],
"buildPresets": [
{
"name": "windows_msvc_base",
"hidden": true,
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Windows"
}
},
{
"name": "windows_msvc_debug",
"inherits": [
"windows_msvc_base"
],
"displayName": "Windows MSVC (Debug)",
"configurePreset": "windows_msvc_debug",
"configuration": "Debug"
},
{
"name": "windows_msvc_release",
"inherits": [
"windows_msvc_base"
],
"displayName": "Windows MSVC (Release)",
"configurePreset": "windows_msvc_release",
"configuration": "Release"
},
{
"name": "windows_msvc_arm64_cross_debug",
"inherits": [
"windows_msvc_base"
],
"displayName": "Windows MSVC (ARM64 cross, Debug)",
"configurePreset": "windows_msvc_arm64_cross_debug",
"configuration": "Debug"
},
{
"name": "windows_msvc_arm64_cross_release",
"inherits": [
"windows_msvc_base"
],
"displayName": "Windows MSVC (ARM64 cross, Release)",
"configurePreset": "windows_msvc_arm64_cross_release",
"configuration": "Release"
},
{
"name": "windows_msvc_ci",
"inherits": [
"windows_msvc_base"
],
"displayName": "Windows MSVC (CI)",
"configurePreset": "windows_msvc_ci",
"configuration": "Release"
},
{
"name": "windows_msvc_arm64_cross_ci",
"inherits": [
"windows_msvc_base"
],
"displayName": "Windows MSVC (ARM64 cross, CI)",
"configurePreset": "windows_msvc_arm64_cross_ci",
"configuration": "Release"
}
],
"testPresets": [
{
"name": "windows_msvc_base",
"hidden": true,
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Windows"
}
},
{
"name": "windows_msvc_debug",
"inherits": [
"base_debug",
"windows_msvc_base"
],
"displayName": "Windows MSVC (Debug)",
"configurePreset": "windows_msvc_debug",
"configuration": "Debug"
},
{
"name": "windows_msvc_release",
"inherits": [
"base_release",
"windows_msvc_base"
],
"displayName": "Windows MSVC (Release)",
"configurePreset": "windows_msvc_release",
"configuration": "Release"
},
{
"name": "windows_msvc_ci",
"inherits": [
"base_release",
"windows_msvc_base"
],
"displayName": "Windows MSVC (CI)",
"configurePreset": "windows_msvc_ci",
"configuration": "Release"
}
],
"workflowPresets": [
{
"name": "windows_msvc_debug",
"displayName": "Windows MSVC (Debug)",
"steps": [
{
"type": "configure",
"name": "windows_msvc_debug"
},
{
"type": "build",
"name": "windows_msvc_debug"
},
{
"type": "test",
"name": "windows_msvc_debug"
}
]
},
{
"name": "windows_msvc",
"displayName": "Windows MSVC (Release)",
"steps": [
{
"type": "configure",
"name": "windows_msvc_release"
},
{
"type": "build",
"name": "windows_msvc_release"
},
{
"type": "test",
"name": "windows_msvc_release"
}
]
},
{
"name": "windows_msvc_arm64_cross_debug",
"displayName": "Windows MSVC (ARM64 cross, Debug)",
"steps": [
{
"type": "configure",
"name": "windows_msvc_arm64_cross_debug"
},
{
"type": "build",
"name": "windows_msvc_arm64_cross_debug"
}
]
},
{
"name": "windows_msvc_arm64_cross",
"displayName": "Windows MSVC (ARM64 cross, Release)",
"steps": [
{
"type": "configure",
"name": "windows_msvc_arm64_cross_release"
},
{
"type": "build",
"name": "windows_msvc_arm64_cross_release"
}
]
},
{
"name": "windows_msvc_ci",
"displayName": "Windows MSVC (CI)",
"steps": [
{
"type": "configure",
"name": "windows_msvc_ci"
},
{
"type": "build",
"name": "windows_msvc_ci"
},
{
"type": "test",
"name": "windows_msvc_ci"
}
]
},
{
"name": "windows_msvc_arm64_cross_ci",
"displayName": "Windows MSVC (ARM64 cross, CI)",
"steps": [
{
"type": "configure",
"name": "windows_msvc_arm64_cross_ci"
},
{
"type": "build",
"name": "windows_msvc_arm64_cross_ci"
}
]
}
]
}

View file

@ -1,177 +0,0 @@
{
"$schema": "https://cmake.org/cmake/help/latest/_downloads/3e2d73bff478d88a7de0de736ba5e361/schema.json",
"version": 8,
"include": [
"commonPresets.json"
],
"configurePresets": [
{
"name": "windows_mingw_base",
"hidden": true,
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Windows"
},
"generator": "Ninja"
},
{
"name": "windows_mingw_debug",
"inherits": [
"base_debug",
"windows_mingw_base"
],
"displayName": "Windows MinGW (Debug)"
},
{
"name": "windows_mingw_release",
"inherits": [
"base_release",
"windows_mingw_base"
],
"displayName": "Windows MinGW (Release)"
},
{
"name": "windows_mingw_ci",
"inherits": [
"base_ci",
"windows_mingw_base"
],
"displayName": "Windows MinGW (CI)"
}
],
"buildPresets": [
{
"name": "windows_mingw_base",
"hidden": true,
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Windows"
}
},
{
"name": "windows_mingw_debug",
"inherits": [
"windows_mingw_base"
],
"displayName": "Windows MinGW (Debug)",
"configurePreset": "windows_mingw_debug"
},
{
"name": "windows_mingw_release",
"inherits": [
"windows_mingw_base"
],
"displayName": "Windows MinGW (Release)",
"configurePreset": "windows_mingw_release"
},
{
"name": "windows_mingw_ci",
"inherits": [
"windows_mingw_base"
],
"displayName": "Windows MinGW (CI)",
"configurePreset": "windows_mingw_ci"
}
],
"testPresets": [
{
"name": "windows_mingw_base",
"hidden": true,
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Windows"
},
"filter": {
"exclude": {
"name": "^example64|example$"
}
}
},
{
"name": "windows_mingw_debug",
"inherits": [
"base_debug",
"windows_mingw_base"
],
"displayName": "Windows MinGW (Debug)",
"configurePreset": "windows_mingw_debug"
},
{
"name": "windows_mingw_release",
"inherits": [
"base_release",
"windows_mingw_base"
],
"displayName": "Windows MinGW (Release)",
"configurePreset": "windows_mingw_release"
},
{
"name": "windows_mingw_ci",
"inherits": [
"base_release",
"windows_mingw_base"
],
"displayName": "Windows MinGW (CI)",
"configurePreset": "windows_mingw_ci"
}
],
"workflowPresets": [
{
"name": "windows_mingw_debug",
"displayName": "Windows MinGW (Debug)",
"steps": [
{
"type": "configure",
"name": "windows_mingw_debug"
},
{
"type": "build",
"name": "windows_mingw_debug"
},
{
"type": "test",
"name": "windows_mingw_debug"
}
]
},
{
"name": "windows_mingw",
"displayName": "Windows MinGW (Release)",
"steps": [
{
"type": "configure",
"name": "windows_mingw_release"
},
{
"type": "build",
"name": "windows_mingw_release"
},
{
"type": "test",
"name": "windows_mingw_release"
}
]
},
{
"name": "windows_mingw_ci",
"displayName": "Windows MinGW (CI)",
"steps": [
{
"type": "configure",
"name": "windows_mingw_ci"
},
{
"type": "build",
"name": "windows_mingw_ci"
},
{
"type": "test",
"name": "windows_mingw_ci"
}
]
}
]
}

View file

@ -18,11 +18,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1761907660,
"narHash": "sha256-kJ8lIZsiPOmbkJypG+B5sReDXSD1KGu2VEPNqhRa/ew=",
"lastModified": 1762977756,
"narHash": "sha256-4PqRErxfe+2toFJFgcRKZ0UI9NSIOJa+7RXVtBhy4KE=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "2fb006b87f04c4d3bdf08cfdbc7fab9c13d94a15",
"rev": "c5ae371f1a6a7fd27823bc500d9390b38c05fa55",
"type": "github"
},
"original": {
@ -32,27 +32,10 @@
"type": "github"
}
},
"qrcodegenerator": {
"flake": false,
"locked": {
"lastModified": 1737616857,
"narHash": "sha256-6SugPt0lp1Gz7nV23FLmsmpfzgFItkSw7jpGftsDPWc=",
"owner": "nayuki",
"repo": "QR-Code-generator",
"rev": "2c9044de6b049ca25cb3cd1649ed7e27aa055138",
"type": "github"
},
"original": {
"owner": "nayuki",
"repo": "QR-Code-generator",
"type": "github"
}
},
"root": {
"inputs": {
"libnbtplusplus": "libnbtplusplus",
"nixpkgs": "nixpkgs",
"qrcodegenerator": "qrcodegenerator"
"nixpkgs": "nixpkgs"
}
}
},

View file

@ -15,11 +15,6 @@
url = "github:PrismLauncher/libnbtplusplus";
flake = false;
};
qrcodegenerator = {
url = "github:nayuki/QR-Code-generator";
flake = false;
};
};
outputs =
@ -27,7 +22,6 @@
self,
nixpkgs,
libnbtplusplus,
qrcodegenerator,
}:
let
@ -175,7 +169,6 @@
prismlauncher-unwrapped = prev.callPackage ./nix/unwrapped.nix {
inherit
libnbtplusplus
qrcodegenerator
self
;
};

View file

@ -108,8 +108,6 @@
#include "icons/IconList.h"
#include "net/HttpMetaCache.h"
#include "java/JavaInstallList.h"
#include "updater/ExternalUpdater.h"
#include "tools/JProfiler.h"
@ -127,7 +125,6 @@
#include <LocalPeer.h>
#include <stdlib.h>
#include <QStringLiteral>
#include "SysInfo.h"
#ifdef Q_OS_LINUX
@ -708,6 +705,16 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_settings->registerSetting("SkinsDir", "skins");
m_settings->registerSetting("JavaDir", "java");
#ifdef Q_OS_MACOS
// Folder security-scoped bookmarks
m_settings->registerSetting("InstanceDirBookmark", "");
m_settings->registerSetting("CentralModsDirBookmark", "");
m_settings->registerSetting("IconsDirBookmark", "");
m_settings->registerSetting("DownloadsDirBookmark", "");
m_settings->registerSetting("SkinsDirBookmark", "");
m_settings->registerSetting("JavaDirBookmark", "");
#endif
// Editors
m_settings->registerSetting("JsonEditor", QString());
@ -958,12 +965,27 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
// Themes
m_themeManager = std::make_unique<ThemeManager>();
#ifdef Q_OS_MACOS
// for macOS: getting directory settings will generate URL security-scoped bookmarks if needed and not present
// this facilitates a smooth transition from a non-sandboxed version of the launcher, that likely can access the directory,
// and a sandboxed version that can't access the directory without a bookmark
// this section can likely be removed once the sandboxed version has been released for a while and migrations aren't done anymore
{
m_settings->get("InstanceDir");
m_settings->get("CentralModsDir");
m_settings->get("IconsDir");
m_settings->get("DownloadsDir");
m_settings->get("SkinsDir");
m_settings->get("JavaDir");
}
#endif
// initialize and load all instances
{
auto InstDirSetting = m_settings->getSetting("InstanceDir");
// instance path: check for problems with '!' in instance path and warn the user in the log
// and remember that we have to show him a dialog when the gui starts (if it does so)
QString instDir = InstDirSetting->get().toString();
QString instDir = m_settings->get("InstanceDir").toString();
qInfo() << "Instance path : " << instDir;
if (FS::checkProblemticPathJava(QDir(instDir))) {
qWarning() << "Your instance path contains \'!\' and this is known to cause java problems!";

View file

@ -1053,8 +1053,6 @@ SET(LAUNCHER_SOURCES
ui/dialogs/ImportResourceDialog.h
ui/dialogs/MSALoginDialog.cpp
ui/dialogs/MSALoginDialog.h
ui/dialogs/OfflineLoginDialog.cpp
ui/dialogs/OfflineLoginDialog.h
ui/dialogs/NewComponentDialog.cpp
ui/dialogs/NewComponentDialog.h
ui/dialogs/NewInstanceDialog.cpp
@ -1081,6 +1079,8 @@ SET(LAUNCHER_SOURCES
ui/dialogs/ResourceUpdateDialog.h
ui/dialogs/InstallLoaderDialog.cpp
ui/dialogs/InstallLoaderDialog.h
ui/dialogs/ChooseOfflineNameDialog.cpp
ui/dialogs/ChooseOfflineNameDialog.h
ui/dialogs/skins/SkinManageDialog.cpp
ui/dialogs/skins/SkinManageDialog.h
@ -1173,6 +1173,15 @@ if (NOT Apple)
)
endif()
if (APPLE)
set(LAUNCHER_SOURCES
${LAUNCHER_SOURCES}
macsandbox/SecurityBookmarkFileAccess.h
macsandbox/SecurityBookmarkFileAccess.mm
)
endif()
if(WIN32)
set(LAUNCHER_SOURCES
console/WindowsConsole.h
@ -1234,13 +1243,13 @@ qt_wrap_ui(LAUNCHER_UI
ui/dialogs/IconPickerDialog.ui
ui/dialogs/ImportResourceDialog.ui
ui/dialogs/MSALoginDialog.ui
ui/dialogs/OfflineLoginDialog.ui
ui/dialogs/AboutDialog.ui
ui/dialogs/ReviewMessageBox.ui
ui/dialogs/ScrollMessageBox.ui
ui/dialogs/BlockedModsDialog.ui
ui/dialogs/ChooseProviderDialog.ui
ui/dialogs/skins/SkinManageDialog.ui
ui/dialogs/ChooseOfflineNameDialog.ui
)
qt_wrap_ui(PRISM_UPDATE_UI
@ -1296,8 +1305,15 @@ target_link_libraries(Launcher_logic
qdcss
BuildConfig
Qt${QT_VERSION_MAJOR}::Widgets
qrcodegenerator
)
if(TARGET PkgConfig::libqrencode)
target_link_libraries(Launcher_logic PkgConfig::libqrencode)
else()
target_include_directories(Launcher_logic PRIVATE ${LIBQRENCODE_INCLUDE_DIR})
target_link_libraries(Launcher_logic ${LIBQRENCODE_LIBRARIES})
endif()
if(TARGET PkgConfig::tomlplusplus)
target_link_libraries(Launcher_logic PkgConfig::tomlplusplus)
else()

View file

@ -59,10 +59,8 @@
#if defined Q_OS_WIN32
#define NOMINMAX
#define WIN32_LEAN_AND_MEAN
#include <objbase.h>
#include <objidl.h>
#include <shlguid.h>
#include <shlobj.h>
#include <shobjidl.h>
#include <sys/utime.h>
#include <versionhelpers.h>

View file

@ -153,7 +153,7 @@ QJsonValue toJson<QVariant>(const QVariant& variant)
template <>
QByteArray requireIsType<QByteArray>(const QJsonValue& value, const QString& what)
{
const QString string = ensureIsType<QString>(value, what);
const QString string = value.toString(what);
// ensure that the string can be safely cast to Latin1
if (string != QString::fromLatin1(string.toLatin1())) {
throw JsonException(what + " is not encodable as Latin1");
@ -221,7 +221,7 @@ QDateTime requireIsType<QDateTime>(const QJsonValue& value, const QString& what)
template <>
QUrl requireIsType<QUrl>(const QJsonValue& value, const QString& what)
{
const QString string = ensureIsType<QString>(value, what);
const QString string = value.toString(what);
if (string.isEmpty()) {
return QUrl();
}
@ -287,7 +287,7 @@ QStringList toStringList(const QString& jsonString)
if (parseError.error != QJsonParseError::NoError || !doc.isArray())
return {};
try {
return ensureIsArrayOf<QString>(doc.array(), "");
return requireIsArrayOf<QString>(doc);
} catch (Json::JsonException& e) {
return {};
}

View file

@ -153,18 +153,6 @@ QUrl requireIsType<QUrl>(const QJsonValue& value, const QString& what);
// the following functions are higher level functions, that make use of the above functions for
// type conversion
template <typename T>
T ensureIsType(const QJsonValue& value, const T default_ = T(), const QString& what = "Value")
{
if (value.isUndefined() || value.isNull()) {
return default_;
}
try {
return requireIsType<T>(value, what);
} catch (const JsonException&) {
return default_;
}
}
/// @throw JsonException
template <typename T>
@ -177,16 +165,6 @@ T requireIsType(const QJsonObject& parent, const QString& key, const QString& wh
return requireIsType<T>(parent.value(key), localWhat);
}
template <typename T>
T ensureIsType(const QJsonObject& parent, const QString& key, const T default_ = T(), const QString& what = "__placeholder__")
{
const QString localWhat = QString(what).replace("__placeholder__", '\'' + key + '\'');
if (!parent.contains(key)) {
return default_;
}
return ensureIsType<T>(parent.value(key), default_, localWhat);
}
template <typename T>
QList<T> requireIsArrayOf(const QJsonDocument& doc)
{
@ -198,26 +176,6 @@ QList<T> requireIsArrayOf(const QJsonDocument& doc)
return out;
}
template <typename T>
QList<T> ensureIsArrayOf(const QJsonValue& value, const QString& what = "Value")
{
const QJsonArray array = ensureIsType<QJsonArray>(value, QJsonArray(), what);
QList<T> out;
for (const QJsonValue val : array) {
out.append(requireIsType<T>(val, what));
}
return out;
}
template <typename T>
QList<T> ensureIsArrayOf(const QJsonValue& value, const QList<T> default_, const QString& what = "Value")
{
if (value.isUndefined()) {
return default_;
}
return ensureIsArrayOf<T>(value, what);
}
/// @throw JsonException
template <typename T>
QList<T> requireIsArrayOf(const QJsonObject& parent, const QString& key, const QString& what = "__placeholder__")
@ -226,20 +184,13 @@ QList<T> requireIsArrayOf(const QJsonObject& parent, const QString& key, const Q
if (!parent.contains(key)) {
throw JsonException(localWhat + "s parent does not contain " + localWhat);
}
return ensureIsArrayOf<T>(parent.value(key), localWhat);
}
template <typename T>
QList<T> ensureIsArrayOf(const QJsonObject& parent,
const QString& key,
const QList<T>& default_ = QList<T>(),
const QString& what = "__placeholder__")
{
const QString localWhat = QString(what).replace("__placeholder__", '\'' + key + '\'');
if (!parent.contains(key)) {
return default_;
const QJsonArray array = parent[key].toArray();
QList<T> out;
for (const QJsonValue val : array) {
out.append(requireIsType<T>(val, "Document"));
}
return ensureIsArrayOf<T>(parent.value(key), default_, localWhat);
return out;
}
// this macro part could be replaced by variadic functions that just pass on their arguments, but that wouldn't work well with IDE helpers
@ -248,18 +199,9 @@ QList<T> ensureIsArrayOf(const QJsonObject& parent,
{ \
return requireIsType<TYPE>(value, what); \
} \
inline TYPE ensure##NAME(const QJsonValue& value, const TYPE default_ = TYPE(), const QString& what = "Value") \
{ \
return ensureIsType<TYPE>(value, default_, what); \
} \
inline TYPE require##NAME(const QJsonObject& parent, const QString& key, const QString& what = "__placeholder__") \
{ \
return requireIsType<TYPE>(parent, key, what); \
} \
inline TYPE ensure##NAME(const QJsonObject& parent, const QString& key, const TYPE default_ = TYPE(), \
const QString& what = "__placeholder") \
{ \
return ensureIsType<TYPE>(parent, key, default_, what); \
}
JSON_HELPERFUNCTIONS(Array, QJsonArray)

View file

@ -61,6 +61,7 @@
#include "JavaCommon.h"
#include "launch/steps/TextPrint.h"
#include "tasks/Task.h"
#include "ui/dialogs/ChooseOfflineNameDialog.h"
LaunchController::LaunchController() : Task() {}
@ -157,10 +158,15 @@ QString LaunchController::askOfflineName(QString playerName, bool demo, bool& ok
QString lastOfflinePlayerName = APPLICATION->settings()->get("LastOfflinePlayerName").toString();
QString usedname = lastOfflinePlayerName.isEmpty() ? playerName : lastOfflinePlayerName;
QString name = QInputDialog::getText(m_parentWidget, tr("Player name"), message, QLineEdit::Normal, usedname, &ok);
if (!ok)
ChooseOfflineNameDialog dialog(message, m_parentWidget);
dialog.setWindowTitle(tr("Player name"));
dialog.setUsername(usedname);
if (dialog.exec() != QDialog::Accepted) {
return {};
if (name.length()) {
}
if (const QString name = dialog.getUsername(); !name.isEmpty()) {
usedname = name;
APPLICATION->settings()->set("LastOfflinePlayerName", usedname);
}

View file

@ -111,8 +111,8 @@ QString getLibraryString()
try {
auto conf = Json::requireDocument(filePath, vkLayer);
auto confObject = Json::requireObject(conf, vkLayer);
auto layer = Json::ensureObject(confObject, "layer");
QString libraryName = Json::ensureString(layer, "library_path");
auto layer = confObject["layer"].toObject();
QString libraryName = layer["library_path"].toString();
if (libraryName.isEmpty()) {
continue;

View file

@ -52,27 +52,27 @@ MetadataPtr parseJavaMeta(const QJsonObject& in)
{
auto meta = std::make_shared<Metadata>();
meta->m_name = Json::ensureString(in, "name", "");
meta->vendor = Json::ensureString(in, "vendor", "");
meta->url = Json::ensureString(in, "url", "");
meta->releaseTime = timeFromS3Time(Json::ensureString(in, "releaseTime", ""));
meta->downloadType = parseDownloadType(Json::ensureString(in, "downloadType", ""));
meta->packageType = Json::ensureString(in, "packageType", "");
meta->runtimeOS = Json::ensureString(in, "runtimeOS", "unknown");
meta->m_name = in["name"].toString("");
meta->vendor = in["vendor"].toString("");
meta->url = in["url"].toString("");
meta->releaseTime = timeFromS3Time(in["releaseTime"].toString(""));
meta->downloadType = parseDownloadType(in["downloadType"].toString(""));
meta->packageType = in["packageType"].toString("");
meta->runtimeOS = in["runtimeOS"].toString("unknown");
if (in.contains("checksum")) {
auto obj = Json::requireObject(in, "checksum");
meta->checksumHash = Json::ensureString(obj, "hash", "");
meta->checksumType = Json::ensureString(obj, "type", "");
meta->checksumHash = obj["hash"].toString("");
meta->checksumType = obj["type"].toString("");
}
if (in.contains("version")) {
auto obj = Json::requireObject(in, "version");
auto name = Json::ensureString(obj, "name", "");
auto major = Json::ensureInteger(obj, "major", 0);
auto minor = Json::ensureInteger(obj, "minor", 0);
auto security = Json::ensureInteger(obj, "security", 0);
auto build = Json::ensureInteger(obj, "build", 0);
auto name = obj["name"].toString("");
auto major = obj["major"].toInteger();
auto minor = obj["minor"].toInteger();
auto security = obj["security"].toInteger();
auto build = obj["build"].toInteger();
meta->version = JavaVersion(major, minor, security, build, name);
}
return meta;

View file

@ -77,27 +77,27 @@ void ManifestDownloadTask::downloadJava(const QJsonDocument& doc)
// valid json doc, begin making jre spot
FS::ensureFolderPathExists(m_final_path);
std::vector<File> toDownload;
auto list = Json::ensureObject(Json::ensureObject(doc.object()), "files");
auto list = doc.object()["files"].toObject();
for (const auto& paths : list.keys()) {
auto file = FS::PathCombine(m_final_path, paths);
const QJsonObject& meta = Json::ensureObject(list, paths);
auto type = Json::ensureString(meta, "type");
const QJsonObject& meta = list[paths].toObject();
auto type = meta["type"].toString();
if (type == "directory") {
FS::ensureFolderPathExists(file);
} else if (type == "link") {
// this is *nix only !
auto path = Json::ensureString(meta, "target");
auto path = meta["target"].toString();
if (!path.isEmpty()) {
QFile::link(path, file);
}
} else if (type == "file") {
// TODO download compressed version if it exists ?
auto raw = Json::ensureObject(Json::ensureObject(meta, "downloads"), "raw");
auto isExec = Json::ensureBoolean(meta, "executable", false);
auto url = Json::ensureString(raw, "url");
auto raw = meta["downloads"].toObject()["raw"].toObject();
auto isExec = meta["executable"].toBool();
auto url = raw["url"].toString();
if (!url.isEmpty() && QUrl(url).isValid()) {
auto f = File{ file, url, QByteArray::fromHex(Json::ensureString(raw, "sha1").toLatin1()), isExec };
auto f = File{ file, url, QByteArray::fromHex(raw["sha1"].toString().toLatin1()), isExec };
toDownload.push_back(f);
}
}
@ -134,4 +134,4 @@ bool ManifestDownloadTask::abort()
emitAborted();
return aborted;
};
} // namespace Java
} // namespace Java

View file

@ -234,29 +234,40 @@ bool LaunchTask::parseXmlLogs(QString const& line, MessageLevel::Enum level)
auto& model = *getLogModel();
model.append(MessageLevel::Error, tr("[Log4j Parse Error] Failed to parse log4j log event: %1").arg(err.value().errMessage));
return false;
} else {
if (!items.isEmpty()) {
auto& model = *getLogModel();
for (auto const& item : items) {
if (std::holds_alternative<LogParser::LogEntry>(item)) {
auto entry = std::get<LogParser::LogEntry>(item);
auto msg = QString("[%1] [%2/%3] [%4]: %5")
.arg(entry.timestamp.toString("HH:mm:ss"))
.arg(entry.thread)
.arg(entry.levelText)
.arg(entry.logger)
.arg(entry.message);
msg = censorPrivateInfo(msg);
model.append(entry.level, msg);
} else if (std::holds_alternative<LogParser::PlainText>(item)) {
auto msg = std::get<LogParser::PlainText>(item).message;
level = LogParser::guessLevel(msg, model.previousLevel());
msg = censorPrivateInfo(msg);
model.append(level, msg);
}
}
}
if (items.isEmpty())
return true;
auto model = getLogModel();
for (auto const& item : items) {
if (std::holds_alternative<LogParser::LogEntry>(item)) {
auto entry = std::get<LogParser::LogEntry>(item);
auto msg = QString("[%1] [%2/%3] [%4]: %5")
.arg(entry.timestamp.toString("HH:mm:ss"))
.arg(entry.thread)
.arg(entry.levelText)
.arg(entry.logger)
.arg(entry.message);
msg = censorPrivateInfo(msg);
model->append(entry.level, msg);
} else if (std::holds_alternative<LogParser::PlainText>(item)) {
auto msg = std::get<LogParser::PlainText>(item).message;
MessageLevel::Enum newLevel = MessageLevel::fromLine(msg);
if (newLevel == MessageLevel::Unknown)
newLevel = LogParser::guessLevel(line);
if (newLevel == MessageLevel::Unknown)
newLevel = model->previousLevel();
msg = censorPrivateInfo(msg);
model->append(newLevel, msg);
}
}
return true;
}
@ -273,23 +284,10 @@ void LaunchTask::onLogLine(QString line, MessageLevel::Enum level)
return;
}
// if the launcher part set a log level, use it
auto innerLevel = MessageLevel::fromLine(line);
if (innerLevel != MessageLevel::Unknown) {
level = innerLevel;
}
auto& model = *getLogModel();
// If the level is still undetermined, guess level
if (level == MessageLevel::Unknown) {
level = LogParser::guessLevel(line, model.previousLevel());
}
// censor private user info
line = censorPrivateInfo(line);
model.append(level, line);
getLogModel()->append(level, line);
}
void LaunchTask::emitSucceeded()

View file

@ -169,8 +169,8 @@ bool LogModel::isOverFlow()
MessageLevel::Enum LogModel::previousLevel()
{
if (!m_content.isEmpty()) {
return m_content.last().level;
if (m_numLines > 0) {
return m_content[m_numLines - 1].level;
}
return MessageLevel::Unknown;
}

View file

@ -320,7 +320,7 @@ std::optional<LogParser::ParsedItem> LogParser::parseLog4J()
throw std::runtime_error("unreachable: already verified this was a complete log4j:Event");
}
MessageLevel::Enum LogParser::guessLevel(const QString& line, MessageLevel::Enum level)
MessageLevel::Enum LogParser::guessLevel(const QString& line)
{
static const QRegularExpression LINE_WITH_LEVEL("^\\[(?<timestamp>[0-9:]+)\\] \\[[^/]+/(?<level>[^\\]]+)\\]");
auto match = LINE_WITH_LEVEL.match(line);
@ -328,24 +328,26 @@ MessageLevel::Enum LogParser::guessLevel(const QString& line, MessageLevel::Enum
// New style logs from log4j
QString timestamp = match.captured("timestamp");
QString levelStr = match.captured("level");
level = MessageLevel::getLevel(levelStr);
return MessageLevel::getLevel(levelStr);
} else {
// Old style forge logs
if (line.contains("[INFO]") || line.contains("[CONFIG]") || line.contains("[FINE]") || line.contains("[FINER]") ||
line.contains("[FINEST]"))
level = MessageLevel::Info;
return MessageLevel::Info;
if (line.contains("[SEVERE]") || line.contains("[STDERR]"))
level = MessageLevel::Error;
return MessageLevel::Error;
if (line.contains("[WARNING]"))
level = MessageLevel::Warning;
return MessageLevel::Warning;
if (line.contains("[DEBUG]"))
level = MessageLevel::Debug;
return MessageLevel::Debug;
}
if (level != MessageLevel::Unknown)
return level;
if (line.contains("overwriting existing"))
return MessageLevel::Fatal;
return MessageLevel::Info;
if (line == "---- Minecraft Crash Report ----")
return MessageLevel::Error;
return MessageLevel::Unknown;
}

View file

@ -59,7 +59,7 @@ class LogParser {
std::optional<Error> getError();
/// guess log level from a line of game log
static MessageLevel::Enum guessLevel(const QString& line, MessageLevel::Enum level);
static MessageLevel::Enum guessLevel(const QString& line);
protected:
std::optional<LogEntry> parseAttributes();

View file

@ -0,0 +1,89 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2024 Kenneth Chew <79120643+kthchew@users.noreply.github.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef FILEACCESS_H
#define FILEACCESS_H
#include <QtCore/QMap>
#include <QtCore/QSet>
Q_FORWARD_DECLARE_OBJC_CLASS(NSData);
Q_FORWARD_DECLARE_OBJC_CLASS(NSURL);
Q_FORWARD_DECLARE_OBJC_CLASS(NSString);
Q_FORWARD_DECLARE_OBJC_CLASS(NSAutoreleasePool);
Q_FORWARD_DECLARE_OBJC_CLASS(NSMutableDictionary);
Q_FORWARD_DECLARE_OBJC_CLASS(NSMutableSet);
class QString;
class QByteArray;
class QUrl;
class SecurityBookmarkFileAccess {
/// The keys are bookmarks and the values are URLs.
NSMutableDictionary* m_bookmarks;
/// The keys are paths and the values are bookmarks.
NSMutableDictionary* m_paths;
/// Contains URLs that are currently being accessed.
NSMutableSet* m_activeURLs;
bool m_readOnly;
NSURL* securityScopedBookmarkToNSURL(QByteArray& bookmark, bool& isStale);
public:
/// \param readOnly A boolean indicating whether the bookmark should be read-only.
SecurityBookmarkFileAccess(bool readOnly = false);
~SecurityBookmarkFileAccess();
/// Get a security scoped bookmark from a URL.
///
/// The URL must be accessible before calling this function. That is, call `startAccessingSecurityScopedResource()` before calling
/// this function. Note that this is called implicitly if the user selects the directory from a file picker.
/// \param url The URL to get the security scoped bookmark from.
/// \return A QByteArray containing the security scoped bookmark.
QByteArray urlToSecurityScopedBookmark(const QUrl& url);
/// Get a security scoped bookmark from a path.
///
/// The path must be accessible before calling this function. That is, call `startAccessingSecurityScopedResource()` before calling
/// this function. Note that this is called implicitly if the user selects the directory from a file picker.
/// \param path The path to get the security scoped bookmark from.
/// \return A QByteArray containing the security scoped bookmark.
QByteArray pathToSecurityScopedBookmark(const QString& path);
/// Get a QUrl from a security scoped bookmark. If the bookmark is stale, isStale will be set to true and the bookmark will be updated.
///
/// You must check whether the URL is valid before using it.
/// \param bookmark The security scoped bookmark to get the URL from.
/// \param isStale A boolean that will be set to true if the bookmark is stale.
/// \return The URL from the security scoped bookmark.
QUrl securityScopedBookmarkToURL(QByteArray& bookmark, bool& isStale);
/// Makes the file or directory at the path pointed to by the bookmark accessible. Unlike `startAccessingSecurityScopedResource()`, this
/// class ensures that only one "access" is active at a time. Calling this function again after the security-scoped resource has
/// already been used will do nothing, and a single call to `stopUsingSecurityScopedBookmark()` will release the resource provided that
/// this is the only `SecurityBookmarkFileAccess` accessing the resource.
///
/// If the bookmark is stale, `isStale` will be set to true and the bookmark will be updated. Stored copies of the bookmark need to be
/// updated.
/// \param bookmark The security scoped bookmark to start accessing.
/// \param isStale A boolean that will be set to true if the bookmark is stale.
/// \return A boolean indicating whether the bookmark was successfully accessed.
bool startUsingSecurityScopedBookmark(QByteArray& bookmark, bool& isStale);
void stopUsingSecurityScopedBookmark(QByteArray& bookmark);
/// Returns true if access to the `path` is currently being maintained by this object.
bool isAccessingPath(const QString& path);
};
#endif //FILEACCESS_H

View file

@ -0,0 +1,172 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2024 Kenneth Chew <79120643+kthchew@users.noreply.github.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "SecurityBookmarkFileAccess.h"
#include <Foundation/Foundation.h>
#include <QByteArray>
#include <QUrl>
QByteArray SecurityBookmarkFileAccess::urlToSecurityScopedBookmark(const QUrl& url)
{
if (!url.isLocalFile())
return {};
NSError* error = nil;
NSURL* nsurl = [url.toNSURL() absoluteURL];
NSData* bookmark;
if ([m_paths objectForKey:[nsurl path]]) {
bookmark = m_paths[[nsurl path]];
} else {
bookmark = [nsurl bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope |
(m_readOnly ? NSURLBookmarkCreationSecurityScopeAllowOnlyReadAccess : 0)
includingResourceValuesForKeys:nil
relativeToURL:nil
error:&error];
}
if (error) {
return {};
}
// remove/reapply access to ensure that write access is immediately cut off for read-only bookmarks
// sometimes you need to call this twice to actually stop access (extra calls aren't harmful)
[nsurl stopAccessingSecurityScopedResource];
[nsurl stopAccessingSecurityScopedResource];
nsurl = [NSURL URLByResolvingBookmarkData:bookmark
options:NSURLBookmarkResolutionWithSecurityScope |
(m_readOnly ? NSURLBookmarkCreationSecurityScopeAllowOnlyReadAccess : 0)
relativeToURL:nil
bookmarkDataIsStale:nil
error:&error];
m_paths[[nsurl path]] = bookmark;
m_bookmarks[bookmark] = nsurl;
QByteArray qBookmark = QByteArray::fromNSData(bookmark);
bool isStale = false;
startUsingSecurityScopedBookmark(qBookmark, isStale);
return qBookmark;
}
SecurityBookmarkFileAccess::SecurityBookmarkFileAccess(bool readOnly) : m_readOnly(readOnly)
{
m_bookmarks = [NSMutableDictionary new];
m_paths = [NSMutableDictionary new];
m_activeURLs = [NSMutableSet new];
}
SecurityBookmarkFileAccess::~SecurityBookmarkFileAccess()
{
for (NSURL* url : m_activeURLs) {
[url stopAccessingSecurityScopedResource];
}
}
QByteArray SecurityBookmarkFileAccess::pathToSecurityScopedBookmark(const QString& path)
{
return urlToSecurityScopedBookmark(QUrl::fromLocalFile(path));
}
NSURL* SecurityBookmarkFileAccess::securityScopedBookmarkToNSURL(QByteArray& bookmark, bool& isStale)
{
NSError* error = nil;
BOOL localStale = NO;
NSURL* nsurl = [NSURL URLByResolvingBookmarkData:bookmark.toNSData()
options:NSURLBookmarkResolutionWithSecurityScope |
(m_readOnly ? NSURLBookmarkCreationSecurityScopeAllowOnlyReadAccess : 0)
relativeToURL:nil
bookmarkDataIsStale:&localStale
error:&error];
if (error) {
return nil;
}
isStale = localStale;
if (isStale) {
NSData* nsBookmark = [nsurl bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope |
(m_readOnly ? NSURLBookmarkCreationSecurityScopeAllowOnlyReadAccess : 0)
includingResourceValuesForKeys:nil
relativeToURL:nil
error:&error];
if (error) {
return nil;
}
bookmark = QByteArray::fromNSData(nsBookmark);
}
NSData* nsBookmark = bookmark.toNSData();
m_paths[[nsurl path]] = nsBookmark;
m_bookmarks[nsBookmark] = nsurl;
return nsurl;
}
QUrl SecurityBookmarkFileAccess::securityScopedBookmarkToURL(QByteArray& bookmark, bool& isStale)
{
if (bookmark.isEmpty())
return {};
NSURL* url = securityScopedBookmarkToNSURL(bookmark, isStale);
if (!url)
return {};
return QUrl::fromNSURL(url);
}
bool SecurityBookmarkFileAccess::startUsingSecurityScopedBookmark(QByteArray& bookmark, bool& isStale)
{
NSURL* url = [m_bookmarks objectForKey:bookmark.toNSData()] ? m_bookmarks[bookmark.toNSData()]
: securityScopedBookmarkToNSURL(bookmark, isStale);
if ([m_activeURLs containsObject:url])
return false;
[url stopAccessingSecurityScopedResource];
if ([url startAccessingSecurityScopedResource]) {
[m_activeURLs addObject:url];
return true;
}
return false;
}
void SecurityBookmarkFileAccess::stopUsingSecurityScopedBookmark(QByteArray& bookmark)
{
if (![m_bookmarks objectForKey:bookmark.toNSData()])
return;
NSURL* url = m_bookmarks[bookmark.toNSData()];
if ([m_activeURLs containsObject:url]) {
[url stopAccessingSecurityScopedResource];
[url stopAccessingSecurityScopedResource];
[m_activeURLs removeObject:url];
[m_paths removeObjectForKey:[url path]];
[m_bookmarks removeObjectForKey:bookmark.toNSData()];
}
}
bool SecurityBookmarkFileAccess::isAccessingPath(const QString& path)
{
NSData* bookmark = [m_paths objectForKey:path.toNSString()];
if (!bookmark && path.endsWith('/')) {
bookmark = [m_paths objectForKey:path.left(path.length() - 1).toNSString()];
}
if (!bookmark) {
return false;
}
NSURL* url = [m_bookmarks objectForKey:bookmark];
return [m_activeURLs containsObject:url];
}

View file

@ -35,34 +35,8 @@
#include "Application.h"
// #define BREAK_INFINITE_LOOP
// #define BREAK_EXCEPTION
// #define BREAK_RETURN
#ifdef BREAK_INFINITE_LOOP
#include <chrono>
#include <thread>
#endif
int main(int argc, char* argv[])
{
#ifdef BREAK_INFINITE_LOOP
while (true) {
std::this_thread::sleep_for(std::chrono::milliseconds(250));
}
#endif
#ifdef BREAK_EXCEPTION
throw 42;
#endif
#ifdef BREAK_RETURN
return 42;
#endif
#if QT_VERSION <= QT_VERSION_CHECK(6, 0, 0)
QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
#endif
// initialize Qt
Application app(argc, argv);

View file

@ -40,8 +40,8 @@ static std::shared_ptr<Index> parseIndexInternal(const QJsonObject& obj)
lists.reserve(objects.size());
std::transform(objects.begin(), objects.end(), std::back_inserter(lists), [](const QJsonObject& obj) {
VersionList::Ptr list = std::make_shared<VersionList>(requireString(obj, "uid"));
list->setName(ensureString(obj, "name", QString()));
list->setSha256(ensureString(obj, "sha256", QString()));
list->setName(obj["name"].toString());
list->setSha256(obj["sha256"].toString());
return list;
});
return std::make_shared<Index>(lists);
@ -52,14 +52,14 @@ static Version::Ptr parseCommonVersion(const QString& uid, const QJsonObject& ob
{
Version::Ptr version = std::make_shared<Version>(uid, requireString(obj, "version"));
version->setTime(QDateTime::fromString(requireString(obj, "releaseTime"), Qt::ISODate).toMSecsSinceEpoch() / 1000);
version->setType(ensureString(obj, "type", QString()));
version->setRecommended(ensureBoolean(obj, QString("recommended"), false));
version->setVolatile(ensureBoolean(obj, QString("volatile"), false));
version->setType(obj["type"].toString());
version->setRecommended(obj["recommended"].toBool());
version->setVolatile(obj["volatile"].toBool());
RequireSet reqs, conflicts;
parseRequires(obj, &reqs, "requires");
parseRequires(obj, &conflicts, "conflicts");
version->setRequires(reqs, conflicts);
if (auto sha256 = ensureString(obj, "sha256", QString()); !sha256.isEmpty()) {
if (auto sha256 = obj["sha256"].toString(); !sha256.isEmpty()) {
version->setSha256(sha256);
}
return version;
@ -89,7 +89,7 @@ static VersionList::Ptr parseVersionListInternal(const QJsonObject& obj)
});
VersionList::Ptr list = std::make_shared<VersionList>(uid);
list->setName(ensureString(obj, "name", QString()));
list->setName(obj["name"].toString());
list->setVersions(versions);
return list;
}
@ -171,8 +171,8 @@ void parseRequires(const QJsonObject& obj, RequireSet* ptr, const char* keyName)
while (iter != reqArray.end()) {
auto reqObject = requireObject(*iter);
auto uid = requireString(reqObject, "uid");
auto equals = ensureString(reqObject, "equals", QString());
auto suggests = ensureString(reqObject, "suggests", QString());
auto equals = reqObject["equals"].toString();
auto suggests = reqObject["suggests"].toString();
ptr->insert({ uid, equals, suggests });
iter++;
}

View file

@ -259,8 +259,8 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument& doc
if (root.contains("runtimes")) {
out->runtimes = {};
for (auto runtime : ensureArray(root, "runtimes")) {
out->runtimes.append(Java::parseJavaMeta(ensureObject(runtime)));
for (auto runtime : root["runtimes"].toArray()) {
out->runtimes.append(Java::parseJavaMeta(runtime.toObject()));
}
}

View file

@ -130,18 +130,18 @@ static ComponentPtr componentFromJsonV1(PackProfile* parent, const QString& comp
auto uid = Json::requireString(obj.value("uid"));
auto filePath = componentJsonPattern.arg(uid);
auto component = makeShared<Component>(parent, uid);
component->m_version = Json::ensureString(obj.value("version"));
component->m_dependencyOnly = Json::ensureBoolean(obj.value("dependencyOnly"), false);
component->m_important = Json::ensureBoolean(obj.value("important"), false);
component->m_version = obj.value("version").toString();
component->m_dependencyOnly = obj.value("dependencyOnly").toBool();
component->m_important = obj.value("important").toBool();
// cached
// TODO @RESILIENCE: ignore invalid values/structure here?
component->m_cachedVersion = Json::ensureString(obj.value("cachedVersion"));
component->m_cachedName = Json::ensureString(obj.value("cachedName"));
component->m_cachedVersion = obj.value("cachedVersion").toString();
component->m_cachedName = obj.value("cachedName").toString();
Meta::parseRequires(obj, &component->m_cachedRequires, "cachedRequires");
Meta::parseRequires(obj, &component->m_cachedConflicts, "cachedConflicts");
component->m_cachedVolatile = Json::ensureBoolean(obj.value("volatile"), false);
bool disabled = Json::ensureBoolean(obj.value("disabled"), false);
component->m_cachedVolatile = obj.value("volatile").toBool();
bool disabled = obj.value("disabled").toBool();
component->setEnabled(!disabled);
return component;
}

View file

@ -41,7 +41,6 @@
#include <QDateTime>
#include <QMap>
#include <QString>
#include <QVariantMap>
enum class Validity { None, Assumed, Certain };

View file

@ -105,9 +105,8 @@ DeviceAuthorizationResponse parseDeviceAuthorizationResponse(const QByteArray& d
}
auto obj = doc.object();
return {
Json::ensureString(obj, "device_code"), Json::ensureString(obj, "user_code"), Json::ensureString(obj, "verification_uri"),
Json::ensureInteger(obj, "expires_in"), Json::ensureInteger(obj, "interval"), Json::ensureString(obj, "error"),
Json::ensureString(obj, "error_description"),
obj["device_code"].toString(), obj["user_code"].toString(), obj["verification_uri"].toString(), obj["expires_in"].toInt(),
obj["interval"].toInt(), obj["error"].toString(), obj["error_description"].toString(),
};
}
@ -217,12 +216,12 @@ AuthenticationResponse parseAuthenticationResponse(const QByteArray& data)
return {};
}
auto obj = doc.object();
return { Json::ensureString(obj, "access_token"),
Json::ensureString(obj, "token_type"),
Json::ensureString(obj, "refresh_token"),
Json::ensureInteger(obj, "expires_in"),
Json::ensureString(obj, "error"),
Json::ensureString(obj, "error_description"),
return { obj["access_token"].toString(),
obj["token_type"].toString(),
obj["refresh_token"].toString(),
obj["expires_in"].toInt(),
obj["error"].toString(),
obj["error_description"].toString(),
obj.toVariantMap() };
}

View file

@ -21,11 +21,11 @@ class QSortFilterProxyModel;
/* A macro to define useful functions to handle Resource* -> T* more easily on derived classes */
#define RESOURCE_HELPERS(T) \
T& at(int index) \
T& at(int index) \
{ \
return *static_cast<T*>(m_resources[index].get()); \
} \
const T& at(int index) const \
const T& at(int index) const \
{ \
return *static_cast<const T*>(m_resources.at(index).get()); \
} \

View file

@ -180,7 +180,7 @@ bool processMCMeta(DataPack* pack, QByteArray&& raw_data)
auto json_doc = QJsonDocument::fromJson(raw_data);
auto pack_obj = Json::requireObject(json_doc.object(), "pack", {});
pack->setPackFormat(Json::ensureInteger(pack_obj, "pack_format", 0));
pack->setPackFormat(pack_obj["pack_format"].toInt());
pack->setDescription(DataPackUtils::processComponent(pack_obj.value("description")));
} catch (Json::JsonException& e) {
qWarning() << "JsonException: " << e.what() << e.cause();
@ -192,19 +192,19 @@ bool processMCMeta(DataPack* pack, QByteArray&& raw_data)
QString buildStyle(const QJsonObject& obj)
{
QStringList styles;
if (auto color = Json::ensureString(obj, "color"); !color.isEmpty()) {
if (auto color = obj["color"].toString(); !color.isEmpty()) {
styles << QString("color: %1;").arg(color);
}
if (obj.contains("bold")) {
QString weight = "normal";
if (Json::ensureBoolean(obj, "bold", false)) {
if (obj["bold"].toBool()) {
weight = "bold";
}
styles << QString("font-weight: %1;").arg(weight);
}
if (obj.contains("italic")) {
QString style = "normal";
if (Json::ensureBoolean(obj, "italic", false)) {
if (obj["italic"].toBool()) {
style = "italic";
}
styles << QString("font-style: %1;").arg(style);
@ -223,10 +223,10 @@ QString processComponent(const QJsonArray& value, bool strikethrough, bool under
QString processComponent(const QJsonObject& obj, bool strikethrough, bool underline)
{
underline = Json::ensureBoolean(obj, "underlined", underline);
strikethrough = Json::ensureBoolean(obj, "strikethrough", strikethrough);
underline = obj["underlined"].toBool(underline);
strikethrough = obj["strikethrough"].toBool(strikethrough);
QString result = Json::ensureString(obj, "text");
QString result = obj["text"].toString();
if (underline) {
result = QString("<u>%1</u>").arg(result);
}
@ -234,14 +234,14 @@ QString processComponent(const QJsonObject& obj, bool strikethrough, bool underl
result = QString("<s>%1</s>").arg(result);
}
// the extra needs to be a array
result += processComponent(Json::ensureArray(obj, "extra"), strikethrough, underline);
result += processComponent(obj["extra"].toArray(), strikethrough, underline);
if (auto style = buildStyle(obj); !style.isEmpty()) {
result = QString("<span %1>%2</span>").arg(style, result);
}
if (obj.contains("clickEvent")) {
auto click_event = Json::ensureObject(obj, "clickEvent");
auto action = Json::ensureString(click_event, "action");
auto value = Json::ensureString(click_event, "value");
auto click_event = obj["clickEvent"].toObject();
auto action = click_event["action"].toString();
auto value = click_event["value"].toString();
if (action == "open_url" && !value.isEmpty()) {
result = QString("<a href=\"%1\">%2</a>").arg(value, result);
}
@ -366,4 +366,4 @@ void LocalDataPackParseTask::executeTask()
}
emitSucceeded();
}
}

View file

@ -75,11 +75,11 @@ ModDetails ReadMCModInfo(QByteArray contents)
val = jsonDoc.object().value("modListVersion");
}
int version = Json::ensureInteger(val, -1);
int version = val.toInt(-1);
// Some mods set the number with "", so it's a String instead
if (version < 0)
version = Json::ensureString(val, "").toInt();
version = val.toString("").toInt();
if (version != 2) {
qWarning() << QString(R"(The value of 'modListVersion' is "%1" (expected "2")! The file may be corrupted.)").arg(version);
@ -298,7 +298,7 @@ ModDetails ReadQuiltModInfo(QByteArray contents)
QJsonParseError jsonError;
QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError);
auto object = Json::requireObject(jsonDoc, "quilt.mod.json");
auto schemaVersion = Json::ensureInteger(object.value("schema_version"), 0, "Quilt schema_version");
auto schemaVersion = object.value("schema_version").toInt();
// https://github.com/QuiltMC/rfcs/blob/be6ba280d785395fefa90a43db48e5bfc1d15eb4/specification/0002-quilt.mod.json.md
if (schemaVersion == 1) {
@ -307,17 +307,17 @@ ModDetails ReadQuiltModInfo(QByteArray contents)
details.mod_id = Json::requireString(modInfo.value("id"), "Mod ID");
details.version = Json::requireString(modInfo.value("version"), "Mod version");
auto modMetadata = Json::ensureObject(modInfo.value("metadata"));
auto modMetadata = modInfo.value("metadata").toObject();
details.name = Json::ensureString(modMetadata.value("name"), details.mod_id);
details.description = Json::ensureString(modMetadata.value("description"));
details.name = modMetadata.value("name").toString(details.mod_id);
details.description = modMetadata.value("description").toString();
auto modContributors = Json::ensureObject(modMetadata.value("contributors"));
auto modContributors = modMetadata.value("contributors").toObject();
// We don't really care about the role of a contributor here
details.authors += modContributors.keys();
auto modContact = Json::ensureObject(modMetadata.value("contact"));
auto modContact = modMetadata.value("contact").toObject();
if (modContact.contains("homepage")) {
details.homeurl = Json::requireString(modContact.value("homepage"));

View file

@ -75,9 +75,9 @@ bool SkinList::update()
try {
auto doc = Json::requireDocument(manifestInfo.absoluteFilePath(), "SkinList JSON file");
const auto root = doc.object();
auto skins = Json::ensureArray(root, "skins");
auto skins = root["skins"].toArray();
for (auto jSkin : skins) {
SkinModel s(m_dir, Json::ensureObject(jSkin));
SkinModel s(m_dir, jSkin.toObject());
if (s.isValid()) {
newSkins << s;
}

View file

@ -21,12 +21,17 @@
#include <QPainter>
#include "FileSystem.h"
#include "Json.h"
static QImage improveSkin(const QImage& skin)
static QImage improveSkin(QImage skin)
{
// It seems some older skins may use this format, which can't be drawn onto
// https://github.com/PrismLauncher/PrismLauncher/issues/4032
// https://doc.qt.io/qt-6/qpainter.html#begin
if (skin.format() == QImage::Format_Indexed8) {
skin = skin.convertToFormat(QImage::Format_RGB32);
}
if (skin.size() == QSize(64, 32)) { // old format
QImage newSkin = QImage(QSize(64, 64), skin.format());
auto newSkin = QImage(QSize(64, 64), skin.format());
newSkin.fill(Qt::transparent);
QPainter p(&newSkin);
p.drawImage(QPoint(0, 0), skin.copy(QRect(0, 0, 64, 32))); // copy head
@ -102,15 +107,15 @@ SkinModel::SkinModel(QString path) : m_path(path), m_texture(getSkin(path)), m_m
}
SkinModel::SkinModel(QDir skinDir, QJsonObject obj)
: m_capeId(Json::ensureString(obj, "capeId")), m_model(Model::CLASSIC), m_url(Json::ensureString(obj, "url"))
: m_capeId(obj["capeId"].toString()), m_model(Model::CLASSIC), m_url(obj["url"].toString())
{
auto name = Json::ensureString(obj, "name");
auto name = obj["name"].toString();
if (auto model = Json::ensureString(obj, "model"); model == "SLIM") {
if (auto model = obj["model"].toString(); model == "SLIM") {
m_model = Model::SLIM;
}
m_path = skinDir.absoluteFilePath(name) + ".png";
m_texture = QImage(getSkin(m_path));
m_texture = getSkin(m_path);
m_preview = generatePreviews(m_texture, m_model == Model::SLIM);
}

View file

@ -374,8 +374,8 @@ Task::Ptr EnsureMetadataTask::flameVersionsTask()
}
for (auto match : data_arr) {
auto match_obj = Json::ensureObject(match, {});
auto file_obj = Json::ensureObject(match_obj, "file", {});
auto match_obj = match.toObject();
auto file_obj = match_obj["file"].toObject();
if (match_obj.isEmpty() || file_obj.isEmpty()) {
qWarning() << "Fingerprint match is empty!";
@ -383,7 +383,7 @@ Task::Ptr EnsureMetadataTask::flameVersionsTask()
return;
}
auto fingerprint = QString::number(Json::ensureVariant(file_obj, "fileFingerprint").toUInt());
auto fingerprint = QString::number(file_obj["fileFingerprint"].toInteger());
auto resource = m_resources.find(fingerprint);
if (resource == m_resources.end()) {
qWarning() << "Invalid fingerprint from the API response.";

View file

@ -31,7 +31,8 @@ static const QMap<QString, IndexedVersionType::VersionType> s_indexed_version_ty
{ "alpha", IndexedVersionType::VersionType::Alpha }
};
static const QList<ModLoaderType> loaderList = { NeoForge, Forge, Cauldron, LiteLoader, Quilt, Fabric, Babric, BTA, LegacyFabric, Ornithe, Rift };
static const QList<ModLoaderType> loaderList = { NeoForge, Forge, Cauldron, LiteLoader, Quilt, Fabric,
Babric, BTA, LegacyFabric, Ornithe, Rift };
QList<ModLoaderType> modLoaderTypesToList(ModLoaderTypes flags)
{

View file

@ -67,7 +67,10 @@ Task::Ptr ResourceAPI::searchProjects(SearchArgs&& args, Callback<QList<ModPlatf
}
callbacks.on_fail(reason, network_error_code);
});
QObject::connect(netJob.get(), &NetJob::aborted, [callbacks] { callbacks.on_abort(); });
QObject::connect(netJob.get(), &NetJob::aborted, [callbacks] {
if (callbacks.on_abort != nullptr)
callbacks.on_abort();
});
return netJob;
}
@ -80,7 +83,7 @@ Task::Ptr ResourceAPI::getProjectVersions(VersionSearchArgs&& args, Callback<QVe
auto versions_url = versions_url_optional.value();
auto netJob = makeShared<NetJob>(QString("%1::Versions").arg(args.pack.name), APPLICATION->network());
auto netJob = makeShared<NetJob>(QString("%1::Versions").arg(args.pack->name), APPLICATION->network());
auto response = std::make_shared<QByteArray>();
netJob->addNetAction(Net::ApiDownload::makeByteArray(versions_url, response));
@ -97,14 +100,14 @@ Task::Ptr ResourceAPI::getProjectVersions(VersionSearchArgs&& args, Callback<QVe
QVector<ModPlatform::IndexedVersion> unsortedVersions;
try {
auto arr = doc.isObject() ? Json::ensureArray(doc.object(), "data") : doc.array();
auto arr = doc.isObject() ? doc.object()["data"].toArray() : doc.array();
for (auto versionIter : arr) {
auto obj = versionIter.toObject();
auto file = loadIndexedPackVersion(obj, args.resourceType);
if (!file.addonId.isValid())
file.addonId = args.pack.addonId;
file.addonId = args.pack->addonId;
if (file.fileId.isValid() && !file.downloadUrl.isEmpty()) // Heuristic to check if the returned value is valid
unsortedVersions.append(file);
@ -135,15 +138,18 @@ Task::Ptr ResourceAPI::getProjectVersions(VersionSearchArgs&& args, Callback<QVe
}
callbacks.on_fail(reason, network_error_code);
});
QObject::connect(netJob.get(), &NetJob::aborted, [callbacks] { callbacks.on_abort(); });
QObject::connect(netJob.get(), &NetJob::aborted, [callbacks] {
if (callbacks.on_abort != nullptr)
callbacks.on_abort();
});
return netJob;
}
Task::Ptr ResourceAPI::getProjectInfo(ProjectInfoArgs&& args, Callback<ModPlatform::IndexedPack>&& callbacks) const
Task::Ptr ResourceAPI::getProjectInfo(ProjectInfoArgs&& args, Callback<ModPlatform::IndexedPack::Ptr>&& callbacks) const
{
auto response = std::make_shared<QByteArray>();
auto job = getProject(args.pack.addonId.toString(), response);
auto job = getProject(args.pack->addonId.toString(), response);
QObject::connect(job.get(), &NetJob::succeeded, [this, response, callbacks, args] {
auto pack = args.pack;
@ -159,8 +165,8 @@ Task::Ptr ResourceAPI::getProjectInfo(ProjectInfoArgs&& args, Callback<ModPlatfo
auto obj = Json::requireObject(doc);
if (obj.contains("data"))
obj = Json::requireObject(obj, "data");
loadIndexedPack(pack, obj);
loadExtraPackInfo(pack, obj);
loadIndexedPack(*pack, obj);
loadExtraPackInfo(*pack, obj);
} catch (const JSONValidationError& e) {
qDebug() << doc;
qWarning() << "Error while reading " << debugName() << " resource info: " << e.cause();
@ -182,7 +188,10 @@ Task::Ptr ResourceAPI::getProjectInfo(ProjectInfoArgs&& args, Callback<ModPlatfo
}
callbacks.on_fail(reason, network_error_code);
});
QObject::connect(job.get(), &NetJob::aborted, [callbacks] { callbacks.on_abort(); });
QObject::connect(job.get(), &NetJob::aborted, [callbacks] {
if (callbacks.on_abort != nullptr)
callbacks.on_abort();
});
return job;
}
@ -213,7 +222,7 @@ Task::Ptr ResourceAPI::getDependencyVersion(DependencySearchArgs&& args, Callbac
if (args.dependency.version.length() != 0 && doc.isObject()) {
arr.append(doc.object());
} else {
arr = doc.isObject() ? Json::ensureArray(doc.object(), "data") : doc.array();
arr = doc.isObject() ? doc.object()["data"].toArray() : doc.array();
}
QVector<ModPlatform::IndexedVersion> versions;

View file

@ -88,7 +88,7 @@ class ResourceAPI {
};
struct VersionSearchArgs {
ModPlatform::IndexedPack pack;
ModPlatform::IndexedPack::Ptr pack;
std::optional<std::list<Version>> mcVersions;
std::optional<ModPlatform::ModLoaderTypes> loaders;
@ -96,7 +96,7 @@ class ResourceAPI {
};
struct ProjectInfoArgs {
ModPlatform::IndexedPack pack;
ModPlatform::IndexedPack::Ptr pack;
};
struct DependencySearchArgs {
@ -115,7 +115,7 @@ class ResourceAPI {
virtual Task::Ptr getProject(QString addonId, std::shared_ptr<QByteArray> response) const;
virtual Task::Ptr getProjects(QStringList addonIds, std::shared_ptr<QByteArray> response) const = 0;
virtual Task::Ptr getProjectInfo(ProjectInfoArgs&&, Callback<ModPlatform::IndexedPack>&&) const;
virtual Task::Ptr getProjectInfo(ProjectInfoArgs&&, Callback<ModPlatform::IndexedPack::Ptr>&&) const;
Task::Ptr getProjectVersions(VersionSearchArgs&& args, Callback<QVector<ModPlatform::IndexedVersion>>&& callbacks) const;
virtual Task::Ptr getDependencyVersion(DependencySearchArgs&&, Callback<ModPlatform::IndexedVersion>&&) const;

View file

@ -33,7 +33,7 @@ enum class ResourceType { Mod, ResourcePack, ShaderPack, Modpack, DataPack, Worl
namespace ResourceTypeUtils {
static const std::set<ResourceType> VALID_RESOURCES = { ResourceType::DataPack, ResourceType::ResourcePack, ResourceType::TexturePack,
ResourceType::ShaderPack, ResourceType::World, ResourceType::Mod };
ResourceType::ShaderPack, ResourceType::World, ResourceType::Mod };
QString getName(ResourceType type);
} // namespace ResourceTypeUtils
} // namespace ModPlatform

View file

@ -40,8 +40,8 @@ void ATLauncher::loadIndexedPack(ATLauncher::IndexedPack& m, QJsonObject& obj)
loadIndexedVersion(version, versionObj);
m.versions.append(version);
}
m.system = Json::ensureBoolean(obj, QString("system"), false);
m.description = Json::ensureString(obj, "description", "");
m.system = obj["system"].toBool();
m.description = obj["description"].toString("");
static const QRegularExpression s_regex("[^A-Za-z0-9]");
m.safeName = Json::requireString(obj, "name").replace(s_regex, "").toLower() + ".png";

View file

@ -100,20 +100,20 @@ static ATLauncher::ModType parseModType(QString rawType)
static void loadVersionLoader(ATLauncher::VersionLoader& p, QJsonObject& obj)
{
p.type = Json::requireString(obj, "type");
p.choose = Json::ensureBoolean(obj, QString("choose"), false);
p.choose = obj["choose"].toBool();
auto metadata = Json::requireObject(obj, "metadata");
p.latest = Json::ensureBoolean(metadata, QString("latest"), false);
p.recommended = Json::ensureBoolean(metadata, QString("recommended"), false);
p.latest = metadata["latest"].toBool();
p.recommended = metadata["recommended"].toBool();
// Minecraft Forge
if (p.type == "forge") {
p.version = Json::ensureString(metadata, "version", "");
if (p.type == "forge" || p.type == "neoforge") {
p.version = metadata["version"].toString("");
}
// Fabric Loader
if (p.type == "fabric") {
p.version = Json::ensureString(metadata, "loader", "");
p.version = metadata["loader"].toString("");
}
}
@ -126,7 +126,7 @@ static void loadVersionLibrary(ATLauncher::VersionLibrary& p, QJsonObject& obj)
p.download_raw = Json::requireString(obj, "download");
p.download = parseDownloadType(p.download_raw);
p.server = Json::ensureString(obj, "server", "");
p.server = obj["server"].toString("");
}
static void loadVersionConfigs(ATLauncher::VersionConfigs& p, QJsonObject& obj)
@ -141,7 +141,7 @@ static void loadVersionMod(ATLauncher::VersionMod& p, QJsonObject& obj)
p.version = Json::requireString(obj, "version");
p.url = Json::requireString(obj, "url");
p.file = Json::requireString(obj, "file");
p.md5 = Json::ensureString(obj, "md5", "");
p.md5 = obj["md5"].toString("");
p.download_raw = Json::requireString(obj, "download");
p.download = parseDownloadType(p.download_raw);
@ -161,7 +161,7 @@ static void loadVersionMod(ATLauncher::VersionMod& p, QJsonObject& obj)
if (obj.contains("extractTo")) {
p.extractTo_raw = Json::requireString(obj, "extractTo");
p.extractTo = parseModType(p.extractTo_raw);
p.extractFolder = Json::ensureString(obj, "extractFolder", "").replace("%s%", "/");
p.extractFolder = obj["extractFolder"].toString("").replace("%s%", "/");
}
if (obj.contains("decompType")) {
@ -170,23 +170,23 @@ static void loadVersionMod(ATLauncher::VersionMod& p, QJsonObject& obj)
p.decompFile = Json::requireString(obj, "decompFile");
}
p.description = Json::ensureString(obj, QString("description"), "");
p.optional = Json::ensureBoolean(obj, QString("optional"), false);
p.recommended = Json::ensureBoolean(obj, QString("recommended"), false);
p.selected = Json::ensureBoolean(obj, QString("selected"), false);
p.hidden = Json::ensureBoolean(obj, QString("hidden"), false);
p.library = Json::ensureBoolean(obj, QString("library"), false);
p.group = Json::ensureString(obj, QString("group"), "");
p.description = obj["description"].toString("");
p.optional = obj["optional"].toBool();
p.recommended = obj["recommended"].toBool();
p.selected = obj["selected"].toBool();
p.hidden = obj["hidden"].toBool();
p.library = obj["library"].toBool();
p.group = obj["group"].toString("");
if (obj.contains("depends")) {
auto dependsArr = Json::requireArray(obj, "depends");
for (const auto depends : dependsArr) {
p.depends.append(Json::requireString(depends));
}
}
p.colour = Json::ensureString(obj, QString("colour"), "");
p.warning = Json::ensureString(obj, QString("warning"), "");
p.colour = obj["colour"].toString("");
p.warning = obj["warning"].toString("");
p.client = Json::ensureBoolean(obj, QString("client"), false);
p.client = obj["client"].toBool();
// computed
p.effectively_hidden = p.hidden || p.library;
@ -194,20 +194,20 @@ static void loadVersionMod(ATLauncher::VersionMod& p, QJsonObject& obj)
static void loadVersionMessages(ATLauncher::VersionMessages& m, QJsonObject& obj)
{
m.install = Json::ensureString(obj, "install", "");
m.update = Json::ensureString(obj, "update", "");
m.install = obj["install"].toString("");
m.update = obj["update"].toString("");
}
static void loadVersionMainClass(ATLauncher::PackVersionMainClass& m, QJsonObject& obj)
{
m.mainClass = Json::ensureString(obj, "mainClass", "");
m.depends = Json::ensureString(obj, "depends", "");
m.mainClass = obj["mainClass"].toString("");
m.depends = obj["depends"].toString("");
}
static void loadVersionExtraArguments(ATLauncher::PackVersionExtraArguments& a, QJsonObject& obj)
{
a.arguments = Json::ensureString(obj, "arguments", "");
a.depends = Json::ensureString(obj, "depends", "");
a.arguments = obj["arguments"].toString("");
a.depends = obj["depends"].toString("");
}
static void loadVersionKeep(ATLauncher::VersionKeep& k, QJsonObject& obj)
@ -272,7 +272,7 @@ void ATLauncher::loadVersion(PackVersion& v, QJsonObject& obj)
{
v.version = Json::requireString(obj, "version");
v.minecraft = Json::requireString(obj, "minecraft");
v.noConfigs = Json::ensureBoolean(obj, QString("noConfigs"), false);
v.noConfigs = obj["noConfigs"].toBool();
if (obj.contains("mainClass")) {
auto main = Json::requireObject(obj, "mainClass");
@ -314,22 +314,22 @@ void ATLauncher::loadVersion(PackVersion& v, QJsonObject& obj)
loadVersionConfigs(v.configs, configsObj);
}
auto colourObj = Json::ensureObject(obj, "colours");
auto colourObj = obj["colours"].toObject();
for (const auto& key : colourObj.keys()) {
v.colours[key] = Json::requireString(colourObj.value(key), "colour");
}
auto warningsObj = Json::ensureObject(obj, "warnings");
auto warningsObj = obj["warnings"].toObject();
for (const auto& key : warningsObj.keys()) {
v.warnings[key] = Json::requireString(warningsObj.value(key), "warning");
}
auto messages = Json::ensureObject(obj, "messages");
auto messages = obj["messages"].toObject();
loadVersionMessages(v.messages, messages);
auto keeps = Json::ensureObject(obj, "keeps");
auto keeps = obj["keeps"].toObject();
loadVersionKeeps(v.keeps, keeps);
auto deletes = Json::ensureObject(obj, "deletes");
auto deletes = obj["deletes"].toObject();
loadVersionDeletes(v.deletes, deletes);
}

View file

@ -59,7 +59,7 @@ QString FlameAPI::getModFileChangelog(int modId, int fileId)
return;
}
changelog = Json::ensureString(doc.object(), "data");
changelog = doc.object()["data"].toString();
});
QObject::connect(netJob.get(), &NetJob::finished, [&lock] { lock.quit(); });
@ -92,7 +92,7 @@ QString FlameAPI::getModDescription(int modId)
return;
}
description = Json::ensureString(doc.object(), "data");
description = doc.object()["data"].toString();
});
QObject::connect(netJob.get(), &NetJob::finished, [&lock] { lock.quit(); });

View file

@ -126,7 +126,7 @@ class FlameAPI : public ResourceAPI {
std::optional<QString> getVersionsURL(VersionSearchArgs const& args) const override
{
auto addonId = args.pack.addonId.toString();
auto addonId = args.pack->addonId.toString();
QString url = QString(BuildConfig.FLAME_BASE_URL + "/mods/%1/files?pageSize=10000").arg(addonId);
if (args.mcVersions.has_value())
@ -140,7 +140,7 @@ class FlameAPI : public ResourceAPI {
return url;
}
QJsonArray documentToArray(QJsonDocument& obj) const override { return Json::ensureArray(obj.object(), "data"); }
QJsonArray documentToArray(QJsonDocument& obj) const override { return obj.object()["data"].toArray(); }
void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) const override { FlameMod::loadIndexedPack(m, obj); }
ModPlatform::IndexedVersion loadIndexedPackVersion(QJsonObject& obj, ModPlatform::ResourceType resourceType) const override
{
@ -160,10 +160,7 @@ class FlameAPI : public ResourceAPI {
void loadExtraPackInfo(ModPlatform::IndexedPack& m, [[maybe_unused]] QJsonObject&) const override { FlameMod::loadBody(m); }
private:
std::optional<QString> getInfoURL(QString const& id) const override
{
return QString(BuildConfig.FLAME_BASE_URL + "/mods/%1").arg(id);
}
std::optional<QString> getInfoURL(QString const& id) const override { return QString(BuildConfig.FLAME_BASE_URL + "/mods/%1").arg(id); }
std::optional<QString> getDependencyURL(DependencySearchArgs const& args) const override
{
auto addonId = args.dependency.addonId.toString();

View file

@ -46,7 +46,9 @@ void FlameCheckUpdate::executeTask()
connect(netJob, &Task::stepProgress, this, &FlameCheckUpdate::propagateStepProgress);
connect(netJob, &Task::details, this, &FlameCheckUpdate::setDetails);
for (auto* resource : m_resources) {
auto versionsUrlOptional = api.getVersionsURL({ { resource->metadata()->project_id.toString() }, m_gameVersions });
auto project = std::make_shared<ModPlatform::IndexedPack>();
project->addonId = resource->metadata()->project_id.toString();
auto versionsUrlOptional = api.getVersionsURL({ project, m_gameVersions });
if (!versionsUrlOptional.has_value())
continue;

View file

@ -15,23 +15,26 @@ void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj)
pack.provider = ModPlatform::ResourceProvider::FLAME;
pack.name = Json::requireString(obj, "name");
pack.slug = Json::requireString(obj, "slug");
pack.websiteUrl = Json::ensureString(Json::ensureObject(obj, "links"), "websiteUrl", "");
pack.description = Json::ensureString(obj, "summary", "");
pack.websiteUrl = obj["links"].toObject()["websiteUrl"].toString("");
pack.description = obj["summary"].toString("");
QJsonObject logo = Json::ensureObject(obj, "logo");
pack.logoName = Json::ensureString(logo, "title");
pack.logoUrl = Json::ensureString(logo, "thumbnailUrl");
QJsonObject logo = obj["logo"].toObject();
pack.logoName = logo["title"].toString();
pack.logoUrl = logo["thumbnailUrl"].toString();
if (pack.logoUrl.isEmpty()) {
pack.logoUrl = Json::ensureString(logo, "url");
pack.logoUrl = logo["url"].toString();
}
auto authors = Json::ensureArray(obj, "authors");
for (auto authorIter : authors) {
auto author = Json::requireObject(authorIter);
ModPlatform::ModpackAuthor packAuthor;
packAuthor.name = Json::requireString(author, "name");
packAuthor.url = Json::requireString(author, "url");
pack.authors.append(packAuthor);
auto authors = obj["authors"].toArray();
if (!authors.isEmpty()) {
pack.authors.clear();
for (auto authorIter : authors) {
auto author = Json::requireObject(authorIter);
ModPlatform::ModpackAuthor packAuthor;
packAuthor.name = Json::requireString(author, "name");
packAuthor.url = Json::requireString(author, "url");
pack.authors.append(packAuthor);
}
}
pack.extraDataLoaded = false;
@ -40,17 +43,17 @@ void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj)
void FlameMod::loadURLs(ModPlatform::IndexedPack& pack, QJsonObject& obj)
{
auto links_obj = Json::ensureObject(obj, "links");
auto links_obj = obj["links"].toObject();
pack.extraData.issuesUrl = Json::ensureString(links_obj, "issuesUrl");
pack.extraData.issuesUrl = links_obj["issuesUrl"].toString();
if (pack.extraData.issuesUrl.endsWith('/'))
pack.extraData.issuesUrl.chop(1);
pack.extraData.sourceUrl = Json::ensureString(links_obj, "sourceUrl");
pack.extraData.sourceUrl = links_obj["sourceUrl"].toString();
if (pack.extraData.sourceUrl.endsWith('/'))
pack.extraData.sourceUrl.chop(1);
pack.extraData.wikiUrl = Json::ensureString(links_obj, "wikiUrl");
pack.extraData.wikiUrl = links_obj["wikiUrl"].toString();
if (pack.extraData.wikiUrl.endsWith('/'))
pack.extraData.wikiUrl.chop(1);
@ -136,7 +139,7 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) ->
file.fileId = Json::requireInteger(obj, "id");
file.date = Json::requireString(obj, "fileDate");
file.version = Json::requireString(obj, "displayName");
file.downloadUrl = Json::ensureString(obj, "downloadUrl");
file.downloadUrl = obj["downloadUrl"].toString();
file.fileName = Json::requireString(obj, "fileName");
file.fileName = FS::RemoveInvalidPathChars(file.fileName);
@ -156,11 +159,11 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) ->
}
file.version_type = ModPlatform::IndexedVersionType(ver_type);
auto hash_list = Json::ensureArray(obj, "hashes");
auto hash_list = obj["hashes"].toArray();
for (auto h : hash_list) {
auto hash_entry = Json::ensureObject(h);
auto hash_entry = h.toObject();
auto hash_types = ModPlatform::ProviderCapabilities::hashType(ModPlatform::ResourceProvider::FLAME);
auto hash_algo = enumToString(Json::ensureInteger(hash_entry, "algo", 1, "algorithm"));
auto hash_algo = enumToString(hash_entry["algo"].toInt(1));
if (hash_types.contains(hash_algo)) {
file.hash = Json::requireString(hash_entry, "value");
file.hash_type = hash_algo;
@ -168,9 +171,9 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) ->
}
}
auto dependencies = Json::ensureArray(obj, "dependencies");
auto dependencies = obj["dependencies"].toArray();
for (auto d : dependencies) {
auto dep = Json::ensureObject(d);
auto dep = d.toObject();
ModPlatform::Dependency dependency;
dependency.addonId = Json::requireInteger(dep, "modId");
switch (Json::requireInteger(dep, "relationType")) {

View file

@ -199,8 +199,8 @@ void FlamePackExportTask::makeApiRequest()
return;
}
for (auto match : dataArr) {
auto matchObj = Json::ensureObject(match, {});
auto fileObj = Json::ensureObject(matchObj, "file", {});
auto matchObj = match.toObject();
auto fileObj = matchObj["file"].toObject();
if (matchObj.isEmpty() || fileObj.isEmpty()) {
qWarning() << "Fingerprint match is empty!";
@ -208,7 +208,7 @@ void FlamePackExportTask::makeApiRequest()
return;
}
auto fingerprint = QString::number(Json::ensureVariant(fileObj, "fileFingerprint").toUInt());
auto fingerprint = QString::number(fileObj["fileFingerprint"].toInteger());
auto mod = pendingHashes.find(fingerprint);
if (mod == pendingHashes.end()) {
qWarning() << "Invalid fingerprint from the API response.";
@ -216,7 +216,7 @@ void FlamePackExportTask::makeApiRequest()
}
setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(mod->name));
if (Json::ensureBoolean(fileObj, "isAvailable", false, "isAvailable"))
if (fileObj["isAvailable"].toBool())
resolvedFiles.insert(mod->path, { Json::requireInteger(fileObj, "modId"), Json::requireInteger(fileObj, "id"),
mod->enabled, mod->isMod });
}
@ -429,4 +429,4 @@ QByteArray FlamePackExportTask::generateHTML()
}
content = "<ul>" + content + "</ul>";
return content.toUtf8();
}
}

View file

@ -5,13 +5,13 @@ static void loadFileV1(Flame::File& f, QJsonObject& file)
{
f.projectId = Json::requireInteger(file, "projectID");
f.fileId = Json::requireInteger(file, "fileID");
f.required = Json::ensureBoolean(file, QString("required"), true);
f.required = file["required"].toBool(true);
}
static void loadModloaderV1(Flame::Modloader& m, QJsonObject& modLoader)
{
m.id = Json::requireString(modLoader, "id");
m.primary = Json::ensureBoolean(modLoader, QString("primary"), false);
m.primary = modLoader["primary"].toBool();
}
static void loadMinecraftV1(Flame::Minecraft& m, QJsonObject& minecraft)
@ -19,15 +19,15 @@ static void loadMinecraftV1(Flame::Minecraft& m, QJsonObject& minecraft)
m.version = Json::requireString(minecraft, "version");
// extra libraries... apparently only used for a custom Minecraft launcher in the 1.2.5 FTB retro pack
// intended use is likely hardcoded in the 'Flame' client, the manifest says nothing
m.libraries = Json::ensureString(minecraft, QString("libraries"), QString());
auto arr = Json::ensureArray(minecraft, "modLoaders", QJsonArray());
m.libraries = minecraft["libraries"].toString();
auto arr = minecraft["modLoaders"].toArray();
for (QJsonValueRef item : arr) {
auto obj = Json::requireObject(item);
Flame::Modloader loader;
loadModloaderV1(loader, obj);
m.modLoaders.append(loader);
}
m.recommendedRAM = Json::ensureInteger(minecraft, "recommendedRam", 0);
m.recommendedRAM = minecraft["recommendedRam"].toInt();
}
static void loadManifestV1(Flame::Manifest& pack, QJsonObject& manifest)
@ -36,11 +36,11 @@ static void loadManifestV1(Flame::Manifest& pack, QJsonObject& manifest)
loadMinecraftV1(pack.minecraft, mc);
pack.name = Json::ensureString(manifest, QString("name"), "Unnamed");
pack.version = Json::ensureString(manifest, QString("version"), QString());
pack.author = Json::ensureString(manifest, QString("author"), "Anonymous");
pack.name = manifest["name"].toString("Unnamed");
pack.version = manifest["version"].toString();
pack.author = manifest["author"].toString("Anonymous");
auto arr = Json::ensureArray(manifest, "files", QJsonArray());
auto arr = manifest["files"].toArray();
for (auto item : arr) {
auto obj = Json::requireObject(item);
@ -50,7 +50,7 @@ static void loadManifestV1(Flame::Manifest& pack, QJsonObject& manifest)
pack.files.insert(file.fileId, file);
}
pack.overrides = Json::ensureString(manifest, "overrides", "overrides");
pack.overrides = manifest["overrides"].toString("overrides");
pack.is_loaded = true;
}

View file

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

View file

@ -8,8 +8,6 @@
#include "meta/VersionList.h"
#include "net/NetJob.h"
#include "net/NetJob.h"
#include <optional>
namespace LegacyFTB {

View file

@ -147,7 +147,7 @@ QList<ModPlatform::Category> ModrinthAPI::loadCategories(std::shared_ptr<QByteAr
for (auto val : arr) {
auto cat = Json::requireObject(val);
auto name = Json::requireString(cat, "name");
if (Json::ensureString(cat, "project_type", "") == projectType)
if (cat["project_type"].toString() == projectType)
categories.push_back({ name, name });
}

View file

@ -45,7 +45,8 @@ class ModrinthAPI : public ResourceAPI {
{
QStringList l;
for (auto loader : { ModPlatform::NeoForge, ModPlatform::Forge, ModPlatform::Fabric, ModPlatform::Quilt, ModPlatform::LiteLoader,
ModPlatform::DataPack, ModPlatform::Babric, ModPlatform::BTA, ModPlatform::LegacyFabric, ModPlatform::Ornithe, ModPlatform::Rift }) {
ModPlatform::DataPack, ModPlatform::Babric, ModPlatform::BTA, ModPlatform::LegacyFabric, ModPlatform::Ornithe,
ModPlatform::Rift }) {
if (types & loader) {
l << getModLoaderAsString(loader);
}
@ -188,7 +189,7 @@ class ModrinthAPI : public ResourceAPI {
get_arguments.append(QString("loaders=[\"%1\"]").arg(getModLoaderStrings(args.loaders.value()).join("\",\"")));
return QString("%1/project/%2/version%3%4")
.arg(BuildConfig.MODRINTH_PROD_URL, args.pack.addonId.toString(), get_arguments.isEmpty() ? "" : "?", get_arguments.join('&'));
.arg(BuildConfig.MODRINTH_PROD_URL, args.pack->addonId.toString(), get_arguments.isEmpty() ? "" : "?", get_arguments.join('&'));
};
QString getGameVersionsArray(std::list<Version> mcVersions) const
@ -204,7 +205,8 @@ class ModrinthAPI : public ResourceAPI {
static inline auto validateModLoaders(ModPlatform::ModLoaderTypes loaders) -> bool
{
return loaders & (ModPlatform::NeoForge | ModPlatform::Forge | ModPlatform::Fabric | ModPlatform::Quilt | ModPlatform::LiteLoader |
ModPlatform::DataPack | ModPlatform::Babric | ModPlatform::BTA | ModPlatform::LegacyFabric | ModPlatform::Ornithe | ModPlatform::Rift);
ModPlatform::DataPack | ModPlatform::Babric | ModPlatform::BTA | ModPlatform::LegacyFabric |
ModPlatform::Ornithe | ModPlatform::Rift);
}
std::optional<QString> getDependencyURL(DependencySearchArgs const& args) const override

View file

@ -249,7 +249,7 @@ bool ModrinthCreationTask::createInstance()
auto root_modpack_url = QUrl::fromLocalFile(root_modpack_path);
// TODO make this work with other sorts of resource
QHash<QString, Resource*> resources;
for (auto file : m_files) {
for (auto& file : m_files) {
auto fileName = file.path;
fileName = FS::RemoveInvalidPathChars(fileName);
auto file_path = FS::PathCombine(root_modpack_path, fileName);
@ -371,8 +371,8 @@ bool ModrinthCreationTask::parseManifest(const QString& index_path,
if (set_internal_data) {
if (m_managed_version_id.isEmpty())
m_managed_version_id = Json::ensureString(obj, "versionId", {}, "Managed ID");
m_managed_name = Json::ensureString(obj, "name", {}, "Managed Name");
m_managed_version_id = obj["versionId"].toString();
m_managed_name = obj["name"].toString();
}
auto jsonFiles = Json::requireIsArrayOf<QJsonObject>(obj, "files", "modrinth.index.json");
@ -381,10 +381,10 @@ bool ModrinthCreationTask::parseManifest(const QString& index_path,
File file;
file.path = Json::requireString(modInfo, "path").replace("\\", "/");
auto env = Json::ensureObject(modInfo, "env");
auto env = modInfo["env"].toObject();
// 'env' field is optional
if (!env.isEmpty()) {
QString support = Json::ensureString(env, "client", "unsupported");
QString support = env["client"].toString("unsupported");
if (support == "unsupported") {
continue;
} else if (support == "optional") {
@ -399,7 +399,7 @@ bool ModrinthCreationTask::parseManifest(const QString& index_path,
// Do not use requireUrl, which uses StrictMode, instead use QUrl's default TolerantMode
// (as Modrinth seems to incorrectly handle spaces)
auto download_arr = Json::ensureArray(modInfo, "downloads");
auto download_arr = modInfo["downloads"].toArray();
for (auto download : download_arr) {
qWarning() << download.toString();
bool is_last = download.toString() == download_arr.last().toString();

View file

@ -33,34 +33,36 @@ bool shouldDownloadOnSide(QString side)
return side == "required" || side == "optional";
}
// https://docs.modrinth.com/api-spec/#tag/projects/operation/getProject
// https://docs.modrinth.com/api/operations/getproject/
void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj)
{
pack.addonId = Json::ensureString(obj, "project_id");
pack.addonId = obj["project_id"].toString();
if (pack.addonId.toString().isEmpty())
pack.addonId = Json::requireString(obj, "id");
pack.provider = ModPlatform::ResourceProvider::MODRINTH;
pack.name = Json::requireString(obj, "title");
pack.slug = Json::ensureString(obj, "slug", "");
pack.slug = obj["slug"].toString("");
if (!pack.slug.isEmpty())
pack.websiteUrl = "https://modrinth.com/mod/" + pack.slug;
else
pack.websiteUrl = "";
pack.description = Json::ensureString(obj, "description", "");
pack.description = obj["description"].toString("");
pack.logoUrl = Json::ensureString(obj, "icon_url", "");
pack.logoName = QString("%1.%2").arg(Json::ensureString(obj, "slug"), QFileInfo(QUrl(pack.logoUrl).fileName()).suffix());
pack.logoUrl = obj["icon_url"].toString("");
pack.logoName = QString("%1.%2").arg(obj["slug"].toString(), QFileInfo(QUrl(pack.logoUrl).fileName()).suffix());
ModPlatform::ModpackAuthor modAuthor;
modAuthor.name = Json::ensureString(obj, "author", QObject::tr("No author(s)"));
modAuthor.url = api.getAuthorURL(modAuthor.name);
pack.authors.append(modAuthor);
if (obj.contains("author")) {
ModPlatform::ModpackAuthor modAuthor;
modAuthor.name = obj["author"].toString();
modAuthor.url = api.getAuthorURL(modAuthor.name);
pack.authors = { modAuthor };
}
auto client = shouldDownloadOnSide(Json::ensureString(obj, "client_side"));
auto server = shouldDownloadOnSide(Json::ensureString(obj, "server_side"));
auto client = shouldDownloadOnSide(obj["client_side"].toString());
auto server = shouldDownloadOnSide(obj["server_side"].toString());
if (server && client) {
pack.side = ModPlatform::Side::UniversalSide;
@ -76,38 +78,38 @@ void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj)
void Modrinth::loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& obj)
{
pack.extraData.issuesUrl = Json::ensureString(obj, "issues_url");
pack.extraData.issuesUrl = obj["issues_url"].toString();
if (pack.extraData.issuesUrl.endsWith('/'))
pack.extraData.issuesUrl.chop(1);
pack.extraData.sourceUrl = Json::ensureString(obj, "source_url");
pack.extraData.sourceUrl = obj["source_url"].toString();
if (pack.extraData.sourceUrl.endsWith('/'))
pack.extraData.sourceUrl.chop(1);
pack.extraData.wikiUrl = Json::ensureString(obj, "wiki_url");
pack.extraData.wikiUrl = obj["wiki_url"].toString();
if (pack.extraData.wikiUrl.endsWith('/'))
pack.extraData.wikiUrl.chop(1);
pack.extraData.discordUrl = Json::ensureString(obj, "discord_url");
pack.extraData.discordUrl = obj["discord_url"].toString();
if (pack.extraData.discordUrl.endsWith('/'))
pack.extraData.discordUrl.chop(1);
auto donate_arr = Json::ensureArray(obj, "donation_urls");
auto donate_arr = obj["donation_urls"].toArray();
for (auto d : donate_arr) {
auto d_obj = Json::requireObject(d);
ModPlatform::DonationData donate;
donate.id = Json::ensureString(d_obj, "id");
donate.platform = Json::ensureString(d_obj, "platform");
donate.url = Json::ensureString(d_obj, "url");
donate.id = d_obj["id"].toString();
donate.platform = d_obj["platform"].toString();
donate.url = d_obj["url"].toString();
pack.extraData.donate.append(donate);
}
pack.extraData.status = Json::ensureString(obj, "status");
pack.extraData.status = obj["status"].toString();
pack.extraData.body = Json::ensureString(obj, "body").remove("<br>");
pack.extraData.body = obj["body"].toString().remove("<br>");
pack.extraDataLoaded = true;
}
@ -147,12 +149,12 @@ ModPlatform::IndexedVersion Modrinth::loadIndexedPackVersion(QJsonObject& obj, Q
file.changelog = Json::requireString(obj, "changelog");
auto dependencies = Json::ensureArray(obj, "dependencies");
auto dependencies = obj["dependencies"].toArray();
for (auto d : dependencies) {
auto dep = Json::ensureObject(d);
auto dep = d.toObject();
ModPlatform::Dependency dependency;
dependency.addonId = Json::ensureString(dep, "project_id");
dependency.version = Json::ensureString(dep, "version_id");
dependency.addonId = dep["project_id"].toString();
dependency.version = dep["version_id"].toString();
auto depType = Json::requireString(dep, "dependency_type");
if (depType == "required")

View file

@ -37,7 +37,7 @@ void loadPack(Pack& v, QJsonObject& obj)
static void loadPackBuildMod(PackBuildMod& b, QJsonObject& obj)
{
b.name = Json::requireString(obj, "name");
b.version = Json::ensureString(obj, "version", "");
b.version = obj["version"].toString("");
b.md5 = Json::requireString(obj, "md5");
b.url = Json::requireString(obj, "url");
}

View file

@ -142,7 +142,7 @@ void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings,
try {
QJsonDocument doc = Json::requireDocument(data);
QJsonObject root = Json::requireObject(doc, "version.json");
QString packMinecraftVersion = Json::ensureString(root, "inheritsFrom", QString(), "");
QString packMinecraftVersion = root["inheritsFrom"].toString();
if (packMinecraftVersion.isEmpty()) {
if (fmlMinecraftVersion.isEmpty()) {
emit failed(tr("Could not understand \"version.json\":\ninheritsFrom is missing"));
@ -151,21 +151,21 @@ void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings,
packMinecraftVersion = fmlMinecraftVersion;
}
components->setComponentVersion("net.minecraft", packMinecraftVersion, true);
for (auto library : Json::ensureArray(root, "libraries", {})) {
for (auto library : root["libraries"].toArray()) {
if (!library.isObject()) {
continue;
}
auto libraryObject = Json::ensureObject(library, {}, "");
auto libraryName = Json::ensureString(libraryObject, "name", "", "");
auto libraryObject = library.toObject();
auto libraryName = libraryObject["name"].toString();
if (libraryName.startsWith("net.neoforged.fancymodloader:")) { // it is neoforge
// no easy way to get the version from the libs so use the arguments
auto arguments = Json::ensureObject(root, "arguments", {});
auto arguments = root["arguments"].toObject();
bool isVersionArg = false;
QString neoforgeVersion;
for (auto arg : Json::ensureArray(arguments, "game", {})) {
auto argument = Json::ensureString(arg, "");
for (auto arg : arguments["game"].toArray()) {
auto argument = arg.toString("");
if (isVersionArg) {
neoforgeVersion = argument;
break;

View file

@ -248,15 +248,15 @@ void HttpMetaCache::Load()
auto root = json.object();
// check file version first
auto version_val = Json::ensureString(root, "version");
auto version_val = root["version"].toString();
if (version_val != "1")
return;
// read the entry array
auto array = Json::ensureArray(root, "entries");
auto array = root["entries"].toArray();
for (auto element : array) {
auto element_obj = Json::ensureObject(element);
auto base = Json::ensureString(element_obj, "base");
auto element_obj = element.toObject();
auto base = element_obj["base"].toString();
if (!m_entries.contains(base))
continue;
@ -264,16 +264,16 @@ void HttpMetaCache::Load()
auto foo = new MetaEntry();
foo->m_baseId = base;
foo->m_relativePath = Json::ensureString(element_obj, "path");
foo->m_md5sum = Json::ensureString(element_obj, "md5sum");
foo->m_etag = Json::ensureString(element_obj, "etag");
foo->m_local_changed_timestamp = Json::ensureDouble(element_obj, "last_changed_timestamp");
foo->m_remote_changed_timestamp = Json::ensureString(element_obj, "remote_changed_timestamp");
foo->m_relativePath = element_obj["path"].toString();
foo->m_md5sum = element_obj["md5sum"].toString();
foo->m_etag = element_obj["etag"].toString();
foo->m_local_changed_timestamp = element_obj["last_changed_timestamp"].toDouble();
foo->m_remote_changed_timestamp = element_obj["remote_changed_timestamp"].toString();
foo->makeEternal(Json::ensureBoolean(element_obj, (const QString)QStringLiteral("eternal"), false));
foo->makeEternal(element_obj[QStringLiteral("eternal")].toBool());
if (!foo->isEternal()) {
foo->m_current_age = Json::ensureDouble(element_obj, "current_age");
foo->m_max_age = Json::ensureDouble(element_obj, "max_age");
foo->m_current_age = element_obj["current_age"].toDouble();
foo->m_max_age = element_obj["max_age"].toDouble();
}
// presumed innocent until closer examination

View file

@ -252,7 +252,6 @@
<file>scalable/discord.svg</file>
<!-- flat instance icons CC BY-SA 4.0, Santiago Cézar -->
<file alias="128x128/flame.png">scalable/instances/flame.svg</file>
<file>scalable/instances/chicken.svg</file>
<file>scalable/instances/creeper.svg</file>
<file>scalable/instances/enderpearl.svg</file>

View file

@ -20,6 +20,12 @@
#include "settings/Setting.h"
#include <QVariant>
#include <QDir>
#include <utility>
#ifdef Q_OS_MACOS
#include "macsandbox/SecurityBookmarkFileAccess.h"
#endif
SettingsObject::SettingsObject(QObject* parent) : QObject(parent) {}
@ -78,9 +84,17 @@ std::shared_ptr<Setting> SettingsObject::getSetting(const QString& id) const
return m_settings[id];
}
QVariant SettingsObject::get(const QString& id) const
QVariant SettingsObject::get(const QString& id)
{
auto setting = getSetting(id);
#ifdef Q_OS_MACOS
// for macOS, use a security scoped bookmark for the paths
if (id.endsWith("Dir")) {
return { getPathFromBookmark(id) };
}
#endif
return (setting ? setting->get() : QVariant());
}
@ -90,11 +104,105 @@ bool SettingsObject::set(const QString& id, QVariant value)
if (!setting) {
qCritical() << QString("Error changing setting %1. Setting doesn't exist.").arg(id);
return false;
} else {
setting->set(value);
}
#ifdef Q_OS_MACOS
// for macOS, keep a security scoped bookmark for the paths
if (value.userType() == QMetaType::QString && id.endsWith("Dir")) {
setPathWithBookmark(id, value.toString());
}
#endif
setting->set(std::move(value));
return true;
}
#ifdef Q_OS_MACOS
QString SettingsObject::getPathFromBookmark(const QString& id)
{
auto setting = getSetting(id);
if (!setting) {
qCritical() << QString("Error changing setting %1. Setting doesn't exist.").arg(id);
return "";
}
// there is no need to use bookmarks if the default value is used or the directory is within the data directory (already can access)
if (setting->get() == setting->defValue() || QDir(setting->get().toString()).absolutePath().startsWith(QDir::current().absolutePath())) {
return setting->get().toString();
}
auto bookmarkId = id + "Bookmark";
auto bookmarkSetting = getSetting(bookmarkId);
if (!bookmarkSetting) {
qCritical() << QString("Error changing setting %1. Bookmark setting doesn't exist.").arg(id);
return "";
}
QByteArray bookmark = bookmarkSetting->get().toByteArray();
if (bookmark.isEmpty()) {
qDebug() << "Creating bookmark for" << id << "at" << setting->get().toString();
setPathWithBookmark(id, setting->get().toString());
return setting->get().toString();
}
bool stale;
QUrl url = m_sandboxedFileAccess.securityScopedBookmarkToURL(bookmark, stale);
if (url.isValid()) {
if (stale) {
setting->set(url.path());
bookmarkSetting->set(bookmark);
}
m_sandboxedFileAccess.startUsingSecurityScopedBookmark(bookmark, stale);
// already did a stale check, no need to do it again
// convert to relative path to current directory if `url` is a descendant of the current directory
QDir currentDir = QDir::current().absolutePath();
return url.path().startsWith(currentDir.absolutePath()) ? currentDir.relativeFilePath(url.path()) : url.path();
}
return setting->get().toString();
}
bool SettingsObject::setPathWithBookmark(const QString& id, const QString& path)
{
auto setting = getSetting(id);
if (!setting) {
qCritical() << QString("Error changing setting %1. Setting doesn't exist.").arg(id);
return false;
}
QDir dir(path);
if (!dir.exists()) {
qCritical() << QString("Error changing setting %1. Path doesn't exist.").arg(id);
return false;
}
QString absolutePath = dir.absolutePath();
QString bookmarkId = id + "Bookmark";
std::shared_ptr<Setting> bookmarkSetting = getSetting(bookmarkId);
// there is no need to use bookmarks if the default value is used or the directory is within the data directory (already can access)
if (path == setting->defValue().toString() || absolutePath.startsWith(QDir::current().absolutePath())) {
bookmarkSetting->reset();
return true;
}
QByteArray bytes = m_sandboxedFileAccess.pathToSecurityScopedBookmark(absolutePath);
if (bytes.isEmpty()) {
qCritical() << QString("Failed to create bookmark for %1 - no access?").arg(id);
// TODO: show an alert to the user asking them to reselect the directory
return false;
}
auto oldBookmark = bookmarkSetting->get().toByteArray();
m_sandboxedFileAccess.stopUsingSecurityScopedBookmark(oldBookmark);
if (!bytes.isEmpty() && bookmarkSetting) {
bookmarkSetting->set(bytes);
bool stale;
m_sandboxedFileAccess.startUsingSecurityScopedBookmark(bytes, stale);
// just created the bookmark, it shouldn't be stale
}
setting->set(path);
return true;
}
#endif
void SettingsObject::reset(const QString& id) const
{

View file

@ -23,6 +23,10 @@
#include <QVariant>
#include <memory>
#ifdef Q_OS_MACOS
#include "macsandbox/SecurityBookmarkFileAccess.h"
#endif
class Setting;
class SettingsObject;
@ -119,7 +123,27 @@ class SettingsObject : public QObject {
* \return The setting's value as a QVariant.
* If no setting with the given ID exists, returns an invalid QVariant.
*/
QVariant get(const QString& id) const;
QVariant get(const QString& id);
#ifdef Q_OS_MACOS
/*!
* \brief Get the path to the file or directory represented by the bookmark stored in the associated setting.
* \param id The setting ID of the relevant directory - this should not include "Bookmark" at the end.
* \return A path to the file or directory represented by the bookmark.
* If a bookmark is not valid or stored, use default logic (directly return the stored path).
* This can attempt to create a bookmark if the path is accessible and the bookmark is not valid.
*/
QString getPathFromBookmark(const QString& id);
/*!
* \brief Set a security-scoped bookmark to the provided path for the associated setting.
* \param id The setting ID of the relevant directory - this should not include "Bookmark" at the end.
* \param path The new desired path.
* \return A boolean indicating whether a bookmark was successfully set.
* The path needs to be accessible to the launcher before calling this function. For example,
* it could come from a user selection in an open panel.
*/
bool setPathWithBookmark(const QString& id, const QString& path);
#endif
/*!
* \brief Sets the value of the setting with the given ID.
@ -207,6 +231,9 @@ class SettingsObject : public QObject {
private:
QMap<QString, std::shared_ptr<Setting>> m_settings;
#ifdef Q_OS_MACOS
SecurityBookmarkFileAccess m_sandboxedFileAccess;
#endif
protected:
bool m_suspendSave = false;

View file

@ -251,8 +251,8 @@ void readIndex(const QString& path, QMap<QString, Language>& languages)
Language lang(iter.key());
auto langObj = Json::requireObject(iter.value());
lang.setTranslationStats(Json::ensureInteger(langObj, "translated", 0), Json::ensureInteger(langObj, "untranslated", 0),
Json::ensureInteger(langObj, "fuzzy", 0));
lang.setTranslationStats(langObj["translated"].toInt(), langObj["untranslated"].toInt(),
langObj["fuzzy"].toInt());
lang.file_name = Json::requireString(langObj, "file");
lang.file_sha1 = Json::requireString(langObj, "sha1");
lang.file_size = Json::requireInteger(langObj, "size");

View file

@ -967,7 +967,7 @@ void MainWindow::processURLs(QList<QUrl> urls)
connect(job.get(), &Task::succeeded, this, [this, array, addonId, fileId, &dl_url, &version] {
qDebug() << "Returned CFURL Json:\n" << array->toStdString().c_str();
auto doc = Json::requireDocument(*array);
auto data = Json::ensureObject(Json::ensureObject(doc.object()), "data");
auto data = doc.object()["data"].toObject();
// No way to find out if it's a mod or a modpack before here
// And also we need to check if it ends with .zip, instead of any better way
version = FlameMod::loadIndexedPackVersion(data);

View file

@ -0,0 +1,74 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2025 Octol1ttle <l1ttleofficial@outlook.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "ChooseOfflineNameDialog.h"
#include <QPushButton>
#include <QRegularExpression>
#include "ui_ChooseOfflineNameDialog.h"
ChooseOfflineNameDialog::ChooseOfflineNameDialog(const QString& message, QWidget* parent) : QDialog(parent), ui(new Ui::ChooseOfflineNameDialog)
{
ui->setupUi(this);
ui->label->setText(message);
ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel"));
ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK"));
const QRegularExpression usernameRegExp("^[A-Za-z0-9_]{3,16}$");
m_usernameValidator = new QRegularExpressionValidator(usernameRegExp, this);
ui->usernameTextBox->setValidator(m_usernameValidator);
connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
}
ChooseOfflineNameDialog::~ChooseOfflineNameDialog()
{
delete ui;
}
QString ChooseOfflineNameDialog::getUsername() const
{
return ui->usernameTextBox->text();
}
void ChooseOfflineNameDialog::setUsername(const QString& username) const
{
ui->usernameTextBox->setText(username);
updateAcceptAllowed(username);
}
void ChooseOfflineNameDialog::updateAcceptAllowed(const QString& username) const
{
const bool allowed = ui->allowInvalidUsernames->isChecked() ? !username.isEmpty() : ui->usernameTextBox->hasAcceptableInput();
ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(allowed);
}
void ChooseOfflineNameDialog::on_usernameTextBox_textEdited(const QString& newText) const
{
updateAcceptAllowed(newText);
}
void ChooseOfflineNameDialog::on_allowInvalidUsernames_checkStateChanged(const Qt::CheckState checkState) const
{
ui->usernameTextBox->setValidator(checkState == Qt::Checked ? nullptr : m_usernameValidator);
updateAcceptAllowed(getUsername());
}

View file

@ -0,0 +1,50 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2025 Octol1ttle <l1ttleofficial@outlook.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QDialog>
#include <QRegularExpressionValidator>
QT_BEGIN_NAMESPACE
namespace Ui {
class ChooseOfflineNameDialog;
}
QT_END_NAMESPACE
class ChooseOfflineNameDialog final : public QDialog {
Q_OBJECT
public:
explicit ChooseOfflineNameDialog(const QString& message, QWidget* parent = nullptr);
~ChooseOfflineNameDialog() override;
QString getUsername() const;
void setUsername(const QString& username) const;
private:
void updateAcceptAllowed(const QString& username) const;
protected slots:
void on_usernameTextBox_textEdited(const QString& newText) const;
void on_allowInvalidUsernames_checkStateChanged(Qt::CheckState checkState) const;
private:
Ui::ChooseOfflineNameDialog* ui;
QRegularExpressionValidator* m_usernameValidator;
};

View file

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ChooseOfflineNameDialog</class>
<widget class="QDialog" name="ChooseOfflineNameDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>158</height>
</rect>
</property>
<property name="windowTitle">
<string>Choose Offline Name</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Message label placeholder.</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="usernameTextBox">
<property name="placeholderText">
<string>Username</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="allowInvalidUsernames">
<property name="toolTip">
<string>A username is valid only if it is from 3 to 16 characters in length, uses English letters, numbers, and underscores. An invalid username may prevent joining servers and singleplayer worlds.</string>
</property>
<property name="text">
<string>Allow invalid usernames</string>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View file

@ -50,7 +50,7 @@
#include <QUrl>
#include <QtWidgets/QPushButton>
#include "qrcodegen.hpp"
#include "qrencode.h"
MSALoginDialog::MSALoginDialog(QWidget* parent) : QDialog(parent), ui(new Ui::MSALoginDialog)
{
@ -146,27 +146,32 @@ void MSALoginDialog::authorizeWithBrowser(const QUrl& url)
m_url = url;
}
// https://stackoverflow.com/questions/21400254/how-to-draw-a-qr-code-with-qt-in-native-c-c
void paintQR(QPainter& painter, const QSize sz, const QString& data, QColor fg)
void paintQR(QPainter& painter, const QSize canvasSize, const QString& data, QColor fg)
{
// NOTE: At this point you will use the API to get the encoding and format you want, instead of my hardcoded stuff:
qrcodegen::QrCode qr = qrcodegen::QrCode::encodeText(data.toUtf8().constData(), qrcodegen::QrCode::Ecc::LOW);
const int s = qr.getSize() > 0 ? qr.getSize() : 1;
const double w = sz.width();
const double h = sz.height();
const double aspect = w / h;
const double size = ((aspect > 1.0) ? h : w);
const double scale = size / (s + 2);
// NOTE: For performance reasons my implementation only draws the foreground parts in supplied color.
// It expects background to be prepared already (in white or whatever is preferred).
const auto* qr = QRcode_encodeString(data.toUtf8().constData(), 0, QRecLevel::QR_ECLEVEL_M, QRencodeMode::QR_MODE_8, 1);
if (!qr) {
qWarning() << "Unable to encode" << data << "as QR code";
return;
}
painter.setPen(Qt::NoPen);
painter.setBrush(fg);
for (int y = 0; y < s; y++) {
for (int x = 0; x < s; x++) {
const int color = qr.getModule(x, y); // 0 for white, 1 for black
if (0 != color) {
const double rx1 = (x + 1) * scale, ry1 = (y + 1) * scale;
QRectF r(rx1, ry1, scale, scale);
// Make sure the QR code fits in the canvas with some padding
const auto qrSize = qr->width;
const auto canvasWidth = canvasSize.width();
const auto canvasHeight = canvasSize.height();
const auto scale = 0.8 * std::min(canvasWidth / qrSize, canvasHeight / qrSize);
// Find an offset to center it in the canvas
const auto offsetX = (canvasWidth - qrSize * scale) / 2;
const auto offsetY = (canvasHeight - qrSize * scale) / 2;
for (int y = 0; y < qrSize; y++) {
for (int x = 0; x < qrSize; x++) {
auto shouldFillIn = qr->data[y * qrSize + x] & 1;
if (shouldFillIn) {
QRectF r(offsetX + x * scale, offsetY + y * scale, scale, scale);
painter.drawRects(&r, 1);
}
}

View file

@ -1,105 +0,0 @@
#include "OfflineLoginDialog.h"
#include "ui_OfflineLoginDialog.h"
#include <QtWidgets/QPushButton>
OfflineLoginDialog::OfflineLoginDialog(QWidget* parent) : QDialog(parent), ui(new Ui::OfflineLoginDialog)
{
ui->setupUi(this);
ui->progressBar->setVisible(false);
ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel"));
ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK"));
connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
}
OfflineLoginDialog::~OfflineLoginDialog()
{
delete ui;
}
// Stage 1: User interaction
void OfflineLoginDialog::accept()
{
setUserInputsEnabled(false);
ui->progressBar->setVisible(true);
// Setup the login task and start it
m_account = MinecraftAccount::createOffline(ui->userTextBox->text());
m_loginTask = m_account->login();
connect(m_loginTask.get(), &Task::failed, this, &OfflineLoginDialog::onTaskFailed);
connect(m_loginTask.get(), &Task::succeeded, this, &OfflineLoginDialog::onTaskSucceeded);
connect(m_loginTask.get(), &Task::status, this, &OfflineLoginDialog::onTaskStatus);
connect(m_loginTask.get(), &Task::progress, this, &OfflineLoginDialog::onTaskProgress);
m_loginTask->start();
}
void OfflineLoginDialog::setUserInputsEnabled(bool enable)
{
ui->userTextBox->setEnabled(enable);
ui->buttonBox->setEnabled(enable);
}
void OfflineLoginDialog::on_allowLongUsernames_stateChanged(int value)
{
if (value == Qt::Checked) {
ui->userTextBox->setMaxLength(INT_MAX);
} else {
ui->userTextBox->setMaxLength(16);
}
}
// Enable the OK button only when the textbox contains something.
void OfflineLoginDialog::on_userTextBox_textEdited(const QString& newText)
{
ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!newText.isEmpty());
}
void OfflineLoginDialog::onTaskFailed(const QString& reason)
{
// Set message
auto lines = reason.split('\n');
QString processed;
for (auto line : lines) {
if (line.size()) {
processed += "<font color='red'>" + line + "</font><br />";
} else {
processed += "<br />";
}
}
ui->label->setText(processed);
// Re-enable user-interaction
setUserInputsEnabled(true);
ui->progressBar->setVisible(false);
}
void OfflineLoginDialog::onTaskSucceeded()
{
QDialog::accept();
}
void OfflineLoginDialog::onTaskStatus(const QString& status)
{
ui->label->setText(status);
}
void OfflineLoginDialog::onTaskProgress(qint64 current, qint64 total)
{
ui->progressBar->setMaximum(total);
ui->progressBar->setValue(current);
}
// Public interface
MinecraftAccountPtr OfflineLoginDialog::newAccount(QWidget* parent, QString msg)
{
OfflineLoginDialog dlg(parent);
dlg.ui->label->setText(msg);
if (dlg.exec() == QDialog::Accepted) {
return dlg.m_account;
}
return nullptr;
}

View file

@ -1,40 +0,0 @@
#pragma once
#include <QtWidgets/QDialog>
#include "minecraft/auth/MinecraftAccount.h"
#include "tasks/Task.h"
namespace Ui {
class OfflineLoginDialog;
}
class OfflineLoginDialog : public QDialog {
Q_OBJECT
public:
~OfflineLoginDialog();
static MinecraftAccountPtr newAccount(QWidget* parent, QString message);
private:
explicit OfflineLoginDialog(QWidget* parent = 0);
void setUserInputsEnabled(bool enable);
protected slots:
void accept();
void onTaskFailed(const QString& reason);
void onTaskSucceeded();
void onTaskStatus(const QString& status);
void onTaskProgress(qint64 current, qint64 total);
void on_userTextBox_textEdited(const QString& newText);
void on_allowLongUsernames_stateChanged(int value);
private:
Ui::OfflineLoginDialog* ui;
MinecraftAccountPtr m_account;
Task::Ptr m_loginTask;
};

View file

@ -1,80 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>OfflineLoginDialog</class>
<widget class="QDialog" name="OfflineLoginDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>150</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Add Account</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="maxLength">
<number>16</number>
</property>
<property name="placeholderText">
<string>Username</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="allowLongUsernames">
<property name="toolTip">
<string>Usernames longer than 16 characters cannot be used for LAN games or offline-mode servers.</string>
</property>
<property name="text">
<string>Allow long usernames</string>
</property>
</widget>
</item>
<item>
<widget class="QProgressBar" name="progressBar">
<property name="value">
<number>69</number>
</property>
<property name="textVisible">
<bool>false</bool>
</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/>
</ui>

View file

@ -461,13 +461,12 @@ void ResourceUpdateDialog::appendResource(CheckUpdateTask::Update const& info, Q
auto requiredByItem = new QTreeWidgetItem(item_top);
if (requiredBy.length() == 1) {
requiredByItem->setText(0, tr("Required by: %1").arg(requiredBy.back()));
requiredByItem->setData(0, Qt::UserRole, requiredBy.back());
} else {
requiredByItem->setText(0, tr("Required by:"));
auto i = 0;
for (auto req : requiredBy) {
auto reqItem = new QTreeWidgetItem(requiredByItem);
reqItem->setText(0, req);
reqItem->insertChildren(i++, { reqItem });
}
}

View file

@ -68,18 +68,14 @@ void ReviewMessageBox::appendResource(ResourceInformation&& info)
filenameItem->setText(0, tr("Filename: %1").arg(info.filename));
filenameItem->setData(0, Qt::UserRole, info.filename);
auto childIndx = 0;
itemTop->insertChildren(childIndx++, { filenameItem });
if (!info.custom_file_path.isEmpty()) {
auto customPathItem = new QTreeWidgetItem(itemTop);
customPathItem->setText(0, tr("This download will be placed in: %1").arg(info.custom_file_path));
itemTop->insertChildren(1, { customPathItem });
customPathItem->setData(0, Qt::UserRole, info.custom_file_path);
itemTop->setIcon(1, QIcon(QIcon::fromTheme("status-yellow")));
itemTop->setToolTip(
childIndx++,
1,
tr("This file will be downloaded to a folder location different from the default, possibly due to its loader requiring it."));
}
@ -87,23 +83,19 @@ void ReviewMessageBox::appendResource(ResourceInformation&& info)
providerItem->setText(0, tr("Provider: %1").arg(info.provider));
providerItem->setData(0, Qt::UserRole, info.provider);
itemTop->insertChildren(childIndx++, { providerItem });
if (!info.required_by.isEmpty()) {
auto requiredByItem = new QTreeWidgetItem(itemTop);
if (info.required_by.length() == 1) {
requiredByItem->setText(0, tr("Required by: %1").arg(info.required_by.back()));
requiredByItem->setData(0, Qt::UserRole, info.required_by.back());
} else {
requiredByItem->setText(0, tr("Required by:"));
auto i = 0;
for (auto req : info.required_by) {
auto reqItem = new QTreeWidgetItem(requiredByItem);
reqItem->setText(0, req);
reqItem->insertChildren(i++, { reqItem });
}
}
itemTop->insertChildren(childIndx++, { requiredByItem });
ui->toggleDepsButton->show();
m_deps << itemTop;
}
@ -112,8 +104,6 @@ void ReviewMessageBox::appendResource(ResourceInformation&& info)
versionTypeItem->setText(0, tr("Version Type: %1").arg(info.version_type));
versionTypeItem->setData(0, Qt::UserRole, info.version_type);
itemTop->insertChildren(childIndx++, { versionTypeItem });
ui->modTreeWidget->addTopLevelItem(itemTop);
}

View file

@ -481,7 +481,7 @@ void SkinManageDialog::on_userBtn_clicked()
return;
}
const auto root = doc.object();
auto id = Json::ensureString(root, "id");
auto id = root["id"].toString();
if (!id.isEmpty()) {
getProfile->setUrl("https://sessionserver.mojang.com/session/minecraft/profile/" + id);
} else {

View file

@ -202,6 +202,13 @@ void SkinOpenGLWindow::resizeGL(int w, int h)
void SkinOpenGLWindow::paintGL()
{
// Adjust the viewport to account for fractional scaling
qreal dpr = devicePixelRatio();
if (dpr != 1.f) {
QSize scaledSize = size() * dpr;
glViewport(0, 0, scaledSize.width(), scaledSize.height());
}
// Clear color and depth buffer
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
@ -235,6 +242,13 @@ void SkinOpenGLWindow::paintGL()
m_scene->draw(m_modelProgram);
m_modelProgram->release();
// Redraw the first frame; this is necessary because the pixel ratio for Wayland fractional scaling is not negotiated properly on the
// first frame
if (m_isFirstFrame) {
m_isFirstFrame = false;
update();
}
}
void SkinOpenGLWindow::updateScene(SkinModel* skin)

View file

@ -74,6 +74,8 @@ class SkinOpenGLWindow : public QOpenGLWindow, protected QOpenGLFunctions {
float m_yaw = 90; // Horizontal rotation angle
float m_pitch = 0; // Vertical rotation angle
bool m_isFirstFrame = true;
opengl::BoxGeometry* m_background = nullptr;
QOpenGLTexture* m_backgroundTexture = nullptr;
QColor m_baseColor;

View file

@ -45,8 +45,8 @@
#include <QDebug>
#include "ui/dialogs/CustomMessageBox.h"
#include "ui/dialogs/ChooseOfflineNameDialog.h"
#include "ui/dialogs/MSALoginDialog.h"
#include "ui/dialogs/OfflineLoginDialog.h"
#include "Application.h"
@ -149,10 +149,13 @@ void AccountListPage::on_actionAddOffline_triggered()
return;
}
MinecraftAccountPtr account =
OfflineLoginDialog::newAccount(this, tr("Please enter your desired username to add your offline account."));
ChooseOfflineNameDialog dialog(tr("Please enter your desired username to add your offline account."), this);
if (dialog.exec() != QDialog::Accepted) {
return;
}
if (account) {
if (const MinecraftAccountPtr account = MinecraftAccount::createOffline(dialog.getUsername())) {
account->login()->start(); // The task will complete here.
m_accounts->addAccount(account);
if (m_accounts->count() == 1) {
m_accounts->setDefaultAccount(account);

View file

@ -6,12 +6,14 @@
#include <QDesktopServices>
#include <QUrl>
#include <QUrlQuery>
#include "modplatform/ModIndex.h"
#include "ui_ManagedPackPage.h"
#include <QFileDialog>
#include <QListView>
#include <QProxyStyle>
#include <QStyleFactory>
#include <memory>
#include "Application.h"
#include "BuildConfig.h"
@ -284,7 +286,8 @@ void ModrinthManagedPackPage::parseManagedPack()
};
callbacks.on_fail = [this](QString reason, int) { setFailState(); };
callbacks.on_abort = [this]() { setFailState(); };
m_fetch_job = m_api.getProjectVersions({ m_pack, {}, {}, ModPlatform::ResourceType::Modpack }, std::move(callbacks));
m_fetch_job = m_api.getProjectVersions(
{ std::make_shared<ModPlatform::IndexedPack>(m_pack), {}, {}, ModPlatform::ResourceType::Modpack }, std::move(callbacks));
ui->changelogTextBrowser->setText(tr("Fetching changelogs..."));
@ -455,7 +458,8 @@ void FlameManagedPackPage::parseManagedPack()
};
callbacks.on_fail = [this](QString reason, int) { setFailState(); };
callbacks.on_abort = [this]() { setFailState(); };
m_fetch_job = m_api.getProjectVersions({ m_pack, {}, {}, ModPlatform::ResourceType::Modpack }, std::move(callbacks));
m_fetch_job = m_api.getProjectVersions(
{ std::make_shared<ModPlatform::IndexedPack>(m_pack), {}, {}, ModPlatform::ResourceType::Modpack }, std::move(callbacks));
m_fetch_job->start();
}

View file

@ -287,16 +287,10 @@ void OtherLogsPage::reload()
if (!m_instance) {
level = MessageLevel::fromLauncherLine(lineTemp);
} else {
// if the launcher part set a log level, use it
auto innerLevel = MessageLevel::fromLine(lineTemp);
if (innerLevel != MessageLevel::Unknown) {
level = innerLevel;
}
level = LogParser::guessLevel(line);
// If the level is still undetermined, guess level
if (level == MessageLevel::StdErr || level == MessageLevel::StdOut || level == MessageLevel::Unknown) {
level = LogParser::guessLevel(line, last);
}
if (level == MessageLevel::Unknown)
level = last;
}
last = level;

View file

@ -23,14 +23,14 @@ ResourceAPI::SearchArgs DataPackResourceModel::createSearchArguments()
ResourceAPI::VersionSearchArgs DataPackResourceModel::createVersionsArguments(const QModelIndex& entry)
{
auto& pack = m_packs[entry.row()];
return { *pack, {}, ModPlatform::ModLoaderType::DataPack };
auto pack = m_packs[entry.row()];
return { pack, {}, ModPlatform::ModLoaderType::DataPack };
}
ResourceAPI::ProjectInfoArgs DataPackResourceModel::createInfoArguments(const QModelIndex& entry)
{
auto& pack = m_packs[entry.row()];
return { *pack };
auto pack = m_packs[entry.row()];
return { pack };
}
void DataPackResourceModel::searchWithTerm(const QString& term, unsigned int sort)

View file

@ -141,13 +141,13 @@ void ImportPage::updateState()
connect(job.get(), &NetJob::succeeded, this, [this, array, addonId, fileId] {
qDebug() << "Returned CFURL Json:\n" << array->toStdString().c_str();
auto doc = Json::requireDocument(*array);
auto data = Json::ensureObject(Json::ensureObject(doc.object()), "data");
auto data = doc.object()["data"].toObject();
// No way to find out if it's a mod or a modpack before here
// And also we need to check if it ends with .zip, instead of any better way
auto fileName = Json::ensureString(data, "fileName");
auto fileName = data["fileName"].toString();
if (fileName.endsWith(".zip")) {
// Have to use ensureString then use QUrl to get proper url encoding
auto dl_url = QUrl(Json::ensureString(data, "downloadUrl", "", "downloadUrl"));
auto dl_url = QUrl(data["downloadUrl"].toString(""));
if (!dl_url.isValid()) {
CustomMessageBox::selectable(
this, tr("Error"),
@ -158,7 +158,7 @@ void ImportPage::updateState()
}
QFileInfo dl_file(dl_url.fileName());
QString pack_name = Json::ensureString(data, "displayName", dl_file.completeBaseName(), "displayName");
QString pack_name = data["displayName"].toString(dl_file.completeBaseName());
QMap<QString, QString> extra_info;
extra_info.insert("pack_id", addonId);

View file

@ -49,7 +49,7 @@ ResourceAPI::SearchArgs ModModel::createSearchArguments()
ResourceAPI::VersionSearchArgs ModModel::createVersionsArguments(const QModelIndex& entry)
{
auto& pack = *m_packs[entry.row()];
auto pack = m_packs[entry.row()];
auto profile = static_cast<MinecraftInstance const&>(m_base_instance).getPackProfile();
Q_ASSERT(profile);
@ -67,7 +67,7 @@ ResourceAPI::VersionSearchArgs ModModel::createVersionsArguments(const QModelInd
ResourceAPI::ProjectInfoArgs ModModel::createInfoArguments(const QModelIndex& entry)
{
auto& pack = *m_packs[entry.row()];
auto pack = m_packs[entry.row()];
return { pack };
}

View file

@ -141,7 +141,7 @@ void ResourceModel::search()
if (m_search_term.startsWith("#")) {
auto projectId = m_search_term.mid(1);
if (!projectId.isEmpty()) {
ResourceAPI::Callback<ModPlatform::IndexedPack> callbacks;
ResourceAPI::Callback<ModPlatform::IndexedPack::Ptr> callbacks;
callbacks.on_fail = [this](QString reason, int) {
if (!s_running_models.constFind(this).value())
@ -159,7 +159,9 @@ void ResourceModel::search()
return;
searchRequestForOneSucceeded(pack);
};
if (auto job = m_api->getProjectInfo({ projectId }, std::move(callbacks)); job)
auto project = std::make_shared<ModPlatform::IndexedPack>();
project->addonId = projectId;
if (auto job = m_api->getProjectInfo({ project }, std::move(callbacks)); job)
runSearchJob(job);
return;
}
@ -219,7 +221,7 @@ void ResourceModel::loadEntry(const QModelIndex& entry)
if (!pack->extraDataLoaded) {
auto args{ createInfoArguments(entry) };
ResourceAPI::Callback<ModPlatform::IndexedPack> callbacks{};
ResourceAPI::Callback<ModPlatform::IndexedPack::Ptr> callbacks{};
callbacks.on_succeed = [this, entry](auto& newpack) {
if (!s_running_models.constFind(this).value())
@ -388,12 +390,12 @@ void ResourceModel::searchRequestSucceeded(QList<ModPlatform::IndexedPack::Ptr>&
endInsertRows();
}
void ResourceModel::searchRequestForOneSucceeded(ModPlatform::IndexedPack& pack)
void ResourceModel::searchRequestForOneSucceeded(ModPlatform::IndexedPack::Ptr pack)
{
m_search_state = SearchState::Finished;
beginInsertRows(QModelIndex(), m_packs.size(), m_packs.size() + 1);
m_packs.append(std::make_shared<ModPlatform::IndexedPack>(pack));
m_packs.append(pack);
endInsertRows();
}
@ -448,18 +450,17 @@ void ResourceModel::versionRequestSucceeded(QVector<ModPlatform::IndexedVersion>
emit versionListUpdated(index);
}
void ResourceModel::infoRequestSucceeded(ModPlatform::IndexedPack& pack, const QModelIndex& index)
void ResourceModel::infoRequestSucceeded(ModPlatform::IndexedPack::Ptr pack, const QModelIndex& index)
{
auto current_pack = data(index, Qt::UserRole).value<ModPlatform::IndexedPack::Ptr>();
// Check if the index is still valid for this resource or not
if (pack.addonId != current_pack->addonId)
if (pack->addonId != current_pack->addonId)
return;
*current_pack = pack;
// Cache info :^)
QVariant new_pack;
new_pack.setValue(current_pack);
new_pack.setValue(pack);
if (!setData(index, new_pack, Qt::UserRole)) {
qWarning() << "Failed to cache resource info!";
return;

View file

@ -138,13 +138,13 @@ class ResourceModel : public QAbstractListModel {
private:
/* Default search request callbacks */
void searchRequestSucceeded(QList<ModPlatform::IndexedPack::Ptr>&);
void searchRequestForOneSucceeded(ModPlatform::IndexedPack&);
void searchRequestForOneSucceeded(ModPlatform::IndexedPack::Ptr);
void searchRequestFailed(QString reason, int network_error_code);
void searchRequestAborted();
void versionRequestSucceeded(QVector<ModPlatform::IndexedVersion>&, QVariant, const QModelIndex&);
void infoRequestSucceeded(ModPlatform::IndexedPack&, const QModelIndex&);
void infoRequestSucceeded(ModPlatform::IndexedPack::Ptr, const QModelIndex&);
signals:
void versionListUpdated(const QModelIndex& index);

View file

@ -25,14 +25,14 @@ ResourceAPI::SearchArgs ResourcePackResourceModel::createSearchArguments()
ResourceAPI::VersionSearchArgs ResourcePackResourceModel::createVersionsArguments(const QModelIndex& entry)
{
auto& pack = m_packs[entry.row()];
return { *pack, {}, {}, ModPlatform::ResourceType::ResourcePack };
auto pack = m_packs[entry.row()];
return { pack, {}, {}, ModPlatform::ResourceType::ResourcePack };
}
ResourceAPI::ProjectInfoArgs ResourcePackResourceModel::createInfoArguments(const QModelIndex& entry)
{
auto& pack = m_packs[entry.row()];
return { *pack };
auto pack = m_packs[entry.row()];
return { pack };
}
void ResourcePackResourceModel::searchWithTerm(const QString& term, unsigned int sort)

View file

@ -22,14 +22,14 @@ ResourceAPI::SearchArgs ShaderPackResourceModel::createSearchArguments()
ResourceAPI::VersionSearchArgs ShaderPackResourceModel::createVersionsArguments(const QModelIndex& entry)
{
auto& pack = m_packs[entry.row()];
return { *pack, {}, {}, ModPlatform::ResourceType::ShaderPack };
auto pack = m_packs[entry.row()];
return { pack, {}, {}, ModPlatform::ResourceType::ShaderPack };
}
ResourceAPI::ProjectInfoArgs ShaderPackResourceModel::createInfoArguments(const QModelIndex& entry)
{
auto& pack = m_packs[entry.row()];
return { *pack };
auto pack = m_packs[entry.row()];
return { pack };
}
void ShaderPackResourceModel::searchWithTerm(const QString& term, unsigned int sort)

View file

@ -11,6 +11,7 @@
#include <Version.h>
#include <QtMath>
#include <memory>
namespace Flame {
@ -167,7 +168,7 @@ void ListModel::performPaginatedSearch()
if (m_currentSearchTerm.startsWith("#")) {
auto projectId = m_currentSearchTerm.mid(1);
if (!projectId.isEmpty()) {
ResourceAPI::Callback<ModPlatform::IndexedPack> callbacks;
ResourceAPI::Callback<ModPlatform::IndexedPack::Ptr> callbacks;
callbacks.on_fail = [this](QString reason, int) { searchRequestFailed(reason); };
callbacks.on_succeed = [this](auto& pack) { searchRequestForOneSucceeded(pack); };
@ -175,7 +176,9 @@ void ListModel::performPaginatedSearch()
qCritical() << "Search task aborted by an unknown reason!";
searchRequestFailed("Aborted");
};
if (auto job = api.getProjectInfo({ { projectId } }, std::move(callbacks)); job) {
auto project = std::make_shared<ModPlatform::IndexedPack>();
project->addonId = projectId;
if (auto job = api.getProjectInfo({ project }, std::move(callbacks)); job) {
m_jobPtr = job;
m_jobPtr->start();
}
@ -189,6 +192,10 @@ void ListModel::performPaginatedSearch()
callbacks.on_succeed = [this](auto& doc) { searchRequestFinished(doc); };
callbacks.on_fail = [this](QString reason, int) { searchRequestFailed(reason); };
callbacks.on_abort = [this] {
qCritical() << "Search task aborted by an unknown reason!";
searchRequestFailed("Aborted");
};
auto netJob = api.searchProjects({ ModPlatform::ResourceType::Modpack, m_nextSearchOffset, m_currentSearchTerm, sort, m_filter->loaders,
m_filter->versions, ModPlatform::Side::NoSide, m_filter->categoryIds, m_filter->openSource },
@ -241,12 +248,12 @@ void Flame::ListModel::searchRequestFinished(QList<ModPlatform::IndexedPack::Ptr
endInsertRows();
}
void Flame::ListModel::searchRequestForOneSucceeded(ModPlatform::IndexedPack& pack)
void Flame::ListModel::searchRequestForOneSucceeded(ModPlatform::IndexedPack::Ptr pack)
{
m_jobPtr.reset();
beginInsertRows(QModelIndex(), m_modpacks.size(), m_modpacks.size() + 1);
m_modpacks.append(std::make_shared<ModPlatform::IndexedPack>(pack));
m_modpacks.append(pack);
endInsertRows();
}

View file

@ -50,7 +50,7 @@ class ListModel : public QAbstractListModel {
void searchRequestFinished(QList<ModPlatform::IndexedPack::Ptr>&);
void searchRequestFailed(QString reason);
void searchRequestForOneSucceeded(ModPlatform::IndexedPack&);
void searchRequestForOneSucceeded(ModPlatform::IndexedPack::Ptr);
private:
void requestLogo(QString file, QString url);

View file

@ -203,7 +203,7 @@ void FlamePage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelInde
CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec();
};
auto netJob = api.getProjectVersions({ *m_current, {}, {}, ModPlatform::ResourceType::Modpack }, std::move(callbacks));
auto netJob = api.getProjectVersions({ m_current, {}, {}, ModPlatform::ResourceType::Modpack }, std::move(callbacks));
m_job = netJob;
netJob->start();

View file

@ -47,6 +47,7 @@
#include "net/ApiDownload.h"
#include <QMessageBox>
#include <memory>
namespace Modrinth {
@ -137,7 +138,7 @@ void ModpackListModel::performPaginatedSearch()
if (m_currentSearchTerm.startsWith("#")) {
auto projectId = m_currentSearchTerm.mid(1);
if (!projectId.isEmpty()) {
ResourceAPI::Callback<ModPlatform::IndexedPack> callbacks;
ResourceAPI::Callback<ModPlatform::IndexedPack::Ptr> callbacks;
callbacks.on_fail = [this](QString reason, int) { searchRequestFailed(reason); };
callbacks.on_succeed = [this](auto& pack) { searchRequestForOneSucceeded(pack); };
@ -145,7 +146,9 @@ void ModpackListModel::performPaginatedSearch()
qCritical() << "Search task aborted by an unknown reason!";
searchRequestFailed("Aborted");
};
if (auto job = api.getProjectInfo({ projectId }, std::move(callbacks)); job) {
auto project = std::make_shared<ModPlatform::IndexedPack>();
project->addonId = projectId;
if (auto job = api.getProjectInfo({ project }, std::move(callbacks)); job) {
m_jobPtr = job;
m_jobPtr->start();
}
@ -159,6 +162,10 @@ void ModpackListModel::performPaginatedSearch()
callbacks.on_succeed = [this](auto& doc) { searchRequestFinished(doc); };
callbacks.on_fail = [this](QString reason, int) { searchRequestFailed(reason); };
callbacks.on_abort = [this] {
qCritical() << "Search task aborted by an unknown reason!";
searchRequestFailed("Aborted");
};
auto netJob = api.searchProjects({ ModPlatform::ResourceType::Modpack, m_nextSearchOffset, m_currentSearchTerm, sort, m_filter->loaders,
m_filter->versions, ModPlatform::Side::NoSide, m_filter->categoryIds, m_filter->openSource },
@ -300,12 +307,12 @@ void ModpackListModel::searchRequestFinished(QList<ModPlatform::IndexedPack::Ptr
endInsertRows();
}
void ModpackListModel::searchRequestForOneSucceeded(ModPlatform::IndexedPack& pack)
void ModpackListModel::searchRequestForOneSucceeded(ModPlatform::IndexedPack::Ptr pack)
{
m_jobPtr.reset();
beginInsertRows(QModelIndex(), m_modpacks.size(), m_modpacks.size() + 1);
m_modpacks.append(std::make_shared<ModPlatform::IndexedPack>(pack));
m_modpacks.append(pack);
endInsertRows();
}

View file

@ -86,7 +86,7 @@ class ModpackListModel : public QAbstractListModel {
public slots:
void searchRequestFinished(QList<ModPlatform::IndexedPack::Ptr>& doc_all);
void searchRequestFailed(QString reason);
void searchRequestForOneSucceeded(ModPlatform::IndexedPack&);
void searchRequestForOneSucceeded(ModPlatform::IndexedPack::Ptr);
protected slots:

View file

@ -146,7 +146,7 @@ void ModrinthPage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelI
if (!m_current->extraDataLoaded) {
qDebug() << "Loading modrinth modpack information";
ResourceAPI::Callback<ModPlatform::IndexedPack> callbacks;
ResourceAPI::Callback<ModPlatform::IndexedPack::Ptr> callbacks;
auto id = m_current->addonId;
callbacks.on_fail = [this](QString reason, int) {
@ -157,10 +157,8 @@ void ModrinthPage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelI
return; // wrong request?
}
*m_current = pack;
QVariant current_updated;
current_updated.setValue(m_current);
current_updated.setValue(pack);
if (!m_model->setData(curr, current_updated, Qt::UserRole))
qWarning() << "Failed to cache extra info for the current pack!";
@ -168,7 +166,7 @@ void ModrinthPage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelI
suggestCurrent();
updateUI();
};
if (auto netJob = m_api.getProjectInfo({ { m_current->addonId } }, std::move(callbacks)); netJob) {
if (auto netJob = m_api.getProjectInfo({ m_current }, std::move(callbacks)); netJob) {
m_job = netJob;
m_job->start();
}
@ -220,7 +218,7 @@ void ModrinthPage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelI
CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec();
};
auto netJob = m_api.getProjectVersions({ *m_current, {}, {}, ModPlatform::ResourceType::Modpack }, std::move(callbacks));
auto netJob = m_api.getProjectVersions({ m_current, {}, {}, ModPlatform::ResourceType::Modpack }, std::move(callbacks));
m_job2 = netJob;
m_job2->start();

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