Merge remote-tracking branch 'upstream/develop' into rework-settings

Signed-off-by: TheKodeToad <TheKodeToad@proton.me>
This commit is contained in:
TheKodeToad 2025-04-15 11:38:31 +01:00
commit 211c71f84a
No known key found for this signature in database
GPG key ID: 5E39D70B4C93C38E
118 changed files with 1541 additions and 1338 deletions

239
.github/workflows/blocked-prs.yml vendored Normal file
View file

@ -0,0 +1,239 @@
name: Blocked/Stacked Pull Requests Automation
on:
pull_request_target:
types:
- opened
- reopened
- edited
- synchronize
workflow_dispatch:
inputs:
pr_id:
description: Local Pull Request number to work on
required: true
type: number
jobs:
blocked_status:
name: Check Blocked Status
runs-on: ubuntu-latest
steps:
- name: Generate token
id: generate-token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ vars.PULL_REQUEST_APP_ID }}
private-key: ${{ secrets.PULL_REQUEST_APP_PRIVATE_KEY }}
- name: Setup From Dispatch Event
if: github.event_name == 'workflow_dispatch'
id: dispatch_event_setup
env:
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
PR_NUMBER: ${{ inputs.pr_id }}
run: |
# setup env for the rest of the workflow
OWNER=$(dirname "${{ github.repository }}")
REPO=$(basename "${{ github.repository }}")
PR_JSON=$(
gh api \
-H "Accept: application/vnd.github.raw+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"/repos/$OWNER/$REPO/pulls/$PR_NUMBER"
)
echo "PR_JSON=$PR_JSON" >> "$GITHUB_ENV"
- name: Setup Environment
id: env_setup
env:
EVENT_PR_JSON: ${{ toJSON(github.event.pull_request) }}
run: |
# setup env for the rest of the workflow
PR_JSON=${PR_JSON:-"$EVENT_PR_JSON"}
{
echo "REPO=$(jq -r '.base.repo.name' <<< "$PR_JSON")"
echo "OWNER=$(jq -r '.base.repo.owner.login' <<< "$PR_JSON")"
echo "PR_NUMBER=$(jq -r '.number' <<< "$PR_JSON")"
echo "JOB_DATA=$(jq -c '
{
"repo": .base.repo.name,
"owner": .base.repo.owner.login,
"repoUrl": .base.repo.html_url,
"prNumber": .number,
"prHeadSha": .head.sha,
"prHeadLabel": .head.label,
"prBody": .body,
"prLabels": (reduce .labels[].name as $l ([]; . + [$l]))
}
' <<< "$PR_JSON")"
} >> "$GITHUB_ENV"
- name: Find Blocked/Stacked PRs in body
id: pr_ids
run: |
prs=$(
jq -c '
.prBody as $body
| (
$body |
reduce (
. | scan("blocked (?:by|on):? #([0-9]+)")
| map({
"type": "Blocked on",
"number": ( . | tonumber )
})
) as $i ([]; . + [$i[]])
) as $bprs
| (
$body |
reduce (
. | scan("stacked on:? #([0-9]+)")
| map({
"type": "Stacked on",
"number": ( . | tonumber )
})
) as $i ([]; . + [$i[]])
) as $sprs
| ($bprs + $sprs) as $prs
| {
"blocking": $prs,
"numBlocking": ( $prs | length),
}
' <<< "$JOB_DATA"
)
echo "prs=$prs" >> "$GITHUB_OUTPUT"
- name: Collect Blocked PR Data
id: blocking_data
if: fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0
env:
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
BLOCKING_PRS: ${{ steps.pr_ids.outputs.prs }}
run: |
blocked_pr_data=$(
while read -r pr_data ; do
gh api \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"/repos/$OWNER/$REPO/pulls/$(jq -r '.number' <<< "$pr_data")" \
| jq -c --arg type "$(jq -r '.type' <<< "$pr_data")" \
'
. | {
"type": $type,
"number": .number,
"merged": .merged,
"labels": (reduce .labels[].name as $l ([]; . + [$l])),
"basePrUrl": .html_url,
"baseRepoName": .head.repo.name,
"baseRepoOwner": .head.repo.owner.login,
"baseRepoUrl": .head.repo.html_url,
"baseSha": .head.sha,
"baseRefName": .head.ref,
}
'
done < <(jq -c '.blocking[]' <<< "$BLOCKING_PRS") | jq -c -s
)
{
echo "data=$blocked_pr_data";
echo "all_merged=$(jq -r 'all(.[].merged; .)' <<< "$blocked_pr_data")";
echo "current_blocking=$(jq -c 'map( select( .merged | not ) | .number )' <<< "$blocked_pr_data" )";
} >> "$GITHUB_OUTPUT"
- name: Add 'blocked' Label is Missing
id: label_blocked
if: (fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0) && !contains(fromJSON(env.JOB_DATA).prLabels, 'blocked') && !fromJSON(steps.blocking_data.outputs.all_merged)
continue-on-error: true
env:
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
run: |
gh -R ${{ github.repository }} issue edit --add-label 'blocked' "$PR_NUMBER"
- name: Remove 'blocked' Label if All Dependencies Are Merged
id: unlabel_blocked
if: fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0 && fromJSON(steps.blocking_data.outputs.all_merged)
continue-on-error: true
env:
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
run: |
gh -R ${{ github.repository }} issue edit --remove-label 'blocked' "$PR_NUMBER"
- name: Apply 'blocking' Label to Unmerged Dependencies
id: label_blocking
if: fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0
continue-on-error: true
env:
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
BLOCKING_ISSUES: ${{ steps.blocking_data.outputs.current_blocking }}
run: |
while read -r pr ; do
gh -R ${{ github.repository }} issue edit --add-label 'blocking' "$pr" || true
done < <(jq -c '.[]' <<< "$BLOCKING_ISSUES")
- name: Apply Blocking PR Status Check
id: blocked_check
if: fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0
continue-on-error: true
env:
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
BLOCKING_DATA: ${{ steps.blocking_data.outputs.data }}
run: |
pr_head_sha=$(jq -r '.prHeadSha' <<< "$JOB_DATA")
# create commit Status, overwrites previous identical context
while read -r pr_data ; do
DESC=$(
jq -r ' "Blocking PR #" + (.number | tostring) + " is " + (if .merged then "" else "not yet " end) + "merged"' <<< "$pr_data"
)
gh api \
--method POST \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"/repos/${OWNER}/${REPO}/statuses/${pr_head_sha}" \
-f "state=$(jq -r 'if .merged then "success" else "failure" end' <<< "$pr_data")" \
-f "target_url=$(jq -r '.basePrUrl' <<< "$pr_data" )" \
-f "description=$DESC" \
-f "context=ci/blocking-pr-check:$(jq '.number' <<< "$pr_data")"
done < <(jq -c '.[]' <<< "$BLOCKING_DATA")
- name: Context Comment
id: generate-comment
if: fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0
continue-on-error: true
env:
BLOCKING_DATA: ${{ steps.blocking_data.outputs.data }}
run: |
COMMENT_PATH="$(pwd)/temp_comment_file.txt"
echo '<h3>PR Dependencies :pushpin:</h3>' > "$COMMENT_PATH"
echo >> "$COMMENT_PATH"
pr_head_label=$(jq -r '.prHeadLabel' <<< "$JOB_DATA")
while read -r pr_data ; do
base_pr=$(jq -r '.number' <<< "$pr_data")
base_ref_name=$(jq -r '.baseRefName' <<< "$pr_data")
base_repo_owner=$(jq -r '.baseRepoOwner' <<< "$pr_data")
base_repo_name=$(jq -r '.baseRepoName' <<< "$pr_data")
compare_url="https://github.com/$base_repo_owner/$base_repo_name/compare/$base_ref_name...$pr_head_label"
status=$(jq -r 'if .merged then ":heavy_check_mark: Merged" else ":x: Not Merged" end' <<< "$pr_data")
type=$(jq -r '.type' <<< "$pr_data")
echo " - $type #$base_pr $status [(compare)]($compare_url)" >> "$COMMENT_PATH"
done < <(jq -c '.[]' <<< "$BLOCKING_DATA")
{
echo 'body<<EOF';
cat "${COMMENT_PATH}";
echo 'EOF';
} >> "$GITHUB_OUTPUT"
- name: 💬 PR Comment
if: fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0
continue-on-error: true
env:
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
COMMENT_BODY: ${{ steps.generate-comment.outputs.body }}
run: |
gh -R ${{ github.repository }} issue comment "$PR_NUMBER" \
--body "$COMMENT_BODY" \
--create-if-none \
--edit-last

View file

@ -52,7 +52,7 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
- os: ubuntu-20.04 - os: ubuntu-22.04
qt_ver: 5 qt_ver: 5
qt_host: linux qt_host: linux
qt_arch: "" qt_arch: ""
@ -63,8 +63,11 @@ jobs:
qt_ver: 6 qt_ver: 6
qt_host: linux qt_host: linux
qt_arch: "" qt_arch: ""
qt_version: "6.5.3" qt_version: "6.8.1"
qt_modules: "qt5compat qtimageformats qtnetworkauth" qt_modules: "qt5compat qtimageformats qtnetworkauth"
linuxdeploy_hash: "4648f278ab3ef31f819e67c30d50f462640e5365a77637d7e6f2ad9fd0b4522a linuxdeploy-x86_64.AppImage"
linuxdeploy_qt_hash: "15106be885c1c48a021198e7e1e9a48ce9d02a86dd0a1848f00bdbf3c1c92724 linuxdeploy-plugin-qt-x86_64.AppImage"
appimageupdate_hash: "f1747cf60058e99f1bb9099ee9787d16c10241313b7acec81810ea1b1e568c11 AppImageUpdate-x86_64.AppImage"
- os: windows-2022 - os: windows-2022
name: "Windows-MinGW-w64" name: "Windows-MinGW-w64"
@ -106,14 +109,6 @@ jobs:
qt_version: "6.8.1" qt_version: "6.8.1"
qt_modules: "qt5compat qtimageformats qtnetworkauth" qt_modules: "qt5compat qtimageformats qtnetworkauth"
- os: macos-14
name: macOS-Legacy
macosx_deployment_target: 10.13
qt_ver: 5
qt_host: mac
qt_version: "5.15.2"
qt_modules: "qtnetworkauth"
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
env: env:
@ -168,9 +163,15 @@ jobs:
with: with:
key: ${{ matrix.os }}-qt${{ matrix.qt_ver }}-${{ matrix.architecture }} key: ${{ matrix.os }}-qt${{ matrix.qt_ver }}-${{ matrix.architecture }}
- name: Use ccache on Debug builds only
if: inputs.build_type == 'Debug'
shell: bash
run: |
echo "CCACHE_VAR=ccache" >> $GITHUB_ENV
- name: Retrieve ccache cache (Windows MinGW-w64) - name: Retrieve ccache cache (Windows MinGW-w64)
if: runner.os == 'Windows' && matrix.msystem != '' && inputs.build_type == 'Debug' if: runner.os == 'Windows' && matrix.msystem != '' && inputs.build_type == 'Debug'
uses: actions/cache@v4.2.2 uses: actions/cache@v4.2.3
with: with:
path: '${{ github.workspace }}\.ccache' path: '${{ github.workspace }}\.ccache'
key: ${{ matrix.os }}-mingw-w64-ccache-${{ github.run_id }} key: ${{ matrix.os }}-mingw-w64-ccache-${{ github.run_id }}
@ -187,11 +188,16 @@ jobs:
ccache -p # Show config ccache -p # Show config
ccache -z # Zero stats ccache -z # Zero stats
- name: Use ccache on Debug builds only - name: Configure ccache (Windows MSVC)
if: inputs.build_type == 'Debug' if: ${{ runner.os == 'Windows' && matrix.msystem == '' && inputs.build_type == 'Debug' }}
shell: bash
run: | run: |
echo "CCACHE_VAR=ccache" >> $GITHUB_ENV # https://github.com/ccache/ccache/wiki/MS-Visual-Studio (I coudn't figure out the compiler prefix)
Copy-Item C:/ProgramData/chocolatey/lib/ccache/tools/ccache-4.7.1-windows-x86_64/ccache.exe -Destination C:/ProgramData/chocolatey/lib/ccache/tools/ccache-4.7.1-windows-x86_64/cl.exe
echo "CLToolExe=cl.exe" >> $env:GITHUB_ENV
echo "CLToolPath=C:/ProgramData/chocolatey/lib/ccache/tools/ccache-4.7.1-windows-x86_64/" >> $env:GITHUB_ENV
echo "TrackFileAccess=false" >> $env:GITHUB_ENV
# Needed for ccache, but also speeds up compile
echo "UseMultiToolTask=true" >> $env:GITHUB_ENV
- name: Set short version - name: Set short version
shell: bash shell: bash
@ -249,14 +255,21 @@ jobs:
- name: Prepare AppImage (Linux) - name: Prepare AppImage (Linux)
if: runner.os == 'Linux' && matrix.qt_ver != 5 if: runner.os == 'Linux' && matrix.qt_ver != 5
env:
APPIMAGEUPDATE_HASH: ${{ matrix.appimageupdate_hash }}
LINUXDEPLOY_HASH: ${{ matrix.linuxdeploy_hash }}
LINUXDEPLOY_QT_HASH: ${{ matrix.linuxdeploy_qt_hash }}
run: | run: |
wget "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage" wget "https://github.com/linuxdeploy/linuxdeploy/releases/download/1-alpha-20250213-2/linuxdeploy-x86_64.AppImage"
wget "https://github.com/linuxdeploy/linuxdeploy-plugin-appimage/releases/download/continuous/linuxdeploy-plugin-appimage-x86_64.AppImage" wget "https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/1-alpha-20250213-1/linuxdeploy-plugin-qt-x86_64.AppImage"
wget "https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage"
wget "https://github.com/AppImageCommunity/AppImageUpdate/releases/download/continuous/AppImageUpdate-x86_64.AppImage" wget "https://github.com/AppImageCommunity/AppImageUpdate/releases/download/2.0.0-alpha-1-20241225/AppImageUpdate-x86_64.AppImage"
sudo apt install libopengl0 libfuse2 sha256sum -c - <<< "$LINUXDEPLOY_HASH"
sha256sum -c - <<< "$LINUXDEPLOY_QT_HASH"
sha256sum -c - <<< "$APPIMAGEUPDATE_HASH"
sudo apt install libopengl0
- name: Add QT_HOST_PATH var (Windows MSVC arm64) - name: Add QT_HOST_PATH var (Windows MSVC arm64)
if: runner.os == 'Windows' && matrix.architecture == 'arm64' if: runner.os == 'Windows' && matrix.architecture == 'arm64'
@ -278,11 +291,6 @@ jobs:
run: | run: |
cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_ENABLE_JAVA_DOWNLOADER=ON -DLauncher_BUILD_PLATFORM=official -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DCMAKE_OSX_ARCHITECTURES="x86_64;arm64" -G Ninja cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_ENABLE_JAVA_DOWNLOADER=ON -DLauncher_BUILD_PLATFORM=official -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DCMAKE_OSX_ARCHITECTURES="x86_64;arm64" -G Ninja
- name: Configure CMake (macOS-Legacy)
if: runner.os == 'macOS' && matrix.qt_ver == 5
run: |
cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_ENABLE_JAVA_DOWNLOADER=ON -DLauncher_BUILD_PLATFORM=official -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DMACOSX_SPARKLE_UPDATE_PUBLIC_KEY="" -DMACOSX_SPARKLE_UPDATE_FEED_URL="" -DCMAKE_OSX_ARCHITECTURES="x86_64" -G Ninja
- name: Configure CMake (Windows MinGW-w64) - name: Configure CMake (Windows MinGW-w64)
if: runner.os == 'Windows' && matrix.msystem != '' if: runner.os == 'Windows' && matrix.msystem != ''
shell: msys2 {0} shell: msys2 {0}
@ -290,19 +298,14 @@ jobs:
cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_ENABLE_JAVA_DOWNLOADER=ON -DLauncher_BUILD_PLATFORM=official -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=6 -DCMAKE_OBJDUMP=/mingw64/bin/objdump.exe -DLauncher_BUILD_ARTIFACT=${{ matrix.name }}-Qt${{ matrix.qt_ver }} -G Ninja cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_ENABLE_JAVA_DOWNLOADER=ON -DLauncher_BUILD_PLATFORM=official -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=6 -DCMAKE_OBJDUMP=/mingw64/bin/objdump.exe -DLauncher_BUILD_ARTIFACT=${{ matrix.name }}-Qt${{ matrix.qt_ver }} -G Ninja
- name: Configure CMake (Windows MSVC) - name: Configure CMake (Windows MSVC)
if: runner.os == 'Windows' && matrix.msystem == '' if: runner.os == 'Windows' && matrix.msystem == '' && matrix.architecture != 'arm64'
run: |
cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_ENABLE_JAVA_DOWNLOADER=ON -DLauncher_BUILD_PLATFORM=official -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DCMAKE_MSVC_RUNTIME_LIBRARY="MultiThreadedDLL" -DLauncher_FORCE_BUNDLED_LIBS=ON -DLauncher_BUILD_ARTIFACT=${{ matrix.name }}-Qt${{ matrix.qt_ver }} -G Ninja
- name: Configure CMake (Windows MSVC arm64)
if: runner.os == 'Windows' && matrix.msystem == '' && matrix.architecture == 'arm64'
run: | run: |
cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_ENABLE_JAVA_DOWNLOADER=ON -DLauncher_BUILD_PLATFORM=official -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DCMAKE_MSVC_RUNTIME_LIBRARY="MultiThreadedDLL" -A${{ matrix.architecture}} -DLauncher_FORCE_BUNDLED_LIBS=ON -DLauncher_BUILD_ARTIFACT=${{ matrix.name }}-Qt${{ matrix.qt_ver }} cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_ENABLE_JAVA_DOWNLOADER=ON -DLauncher_BUILD_PLATFORM=official -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DCMAKE_MSVC_RUNTIME_LIBRARY="MultiThreadedDLL" -A${{ matrix.architecture}} -DLauncher_FORCE_BUNDLED_LIBS=ON -DLauncher_BUILD_ARTIFACT=${{ matrix.name }}-Qt${{ matrix.qt_ver }}
# https://github.com/ccache/ccache/wiki/MS-Visual-Studio (I coudn't figure out the compiler prefix)
if ("${{ env.CCACHE_VAR }}")
{
Copy-Item C:/ProgramData/chocolatey/lib/ccache/tools/ccache-4.7.1-windows-x86_64/ccache.exe -Destination C:/ProgramData/chocolatey/lib/ccache/tools/ccache-4.7.1-windows-x86_64/cl.exe
echo "CLToolExe=cl.exe" >> $env:GITHUB_ENV
echo "CLToolPath=C:/ProgramData/chocolatey/lib/ccache/tools/ccache-4.7.1-windows-x86_64/" >> $env:GITHUB_ENV
echo "TrackFileAccess=false" >> $env:GITHUB_ENV
}
# Needed for ccache, but also speeds up compile
echo "UseMultiToolTask=true" >> $env:GITHUB_ENV
- name: Configure CMake (Linux) - name: Configure CMake (Linux)
if: runner.os == 'Linux' if: runner.os == 'Linux'

56
.github/workflows/merge-blocking-pr.yml vendored Normal file
View file

@ -0,0 +1,56 @@
name: Merged Blocking Pull Request Automation
on:
pull_request:
types:
- closed
jobs:
update-blocked-status:
name: Update Blocked Status
runs-on: ubuntu-latest
# a pr that was a `blocking:<id>` label was merged.
# find the open pr's it was blocked by and trigger a refresh of their state
if: github.event.pull_request.merged == true && contains( join( github.event.pull_request.labels.*.name, ',' ), 'blocking' )
steps:
- name: Generate token
id: generate-token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ vars.PULL_REQUEST_APP_ID }}
private-key: ${{ secrets.PULL_REQUEST_APP_PRIVATE_KEY }}
- name: Gather Dependent PRs
id: gather_deps
env:
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
blocked_prs=$(
gh -R ${{ github.repository }} pr list --label 'blocked' --json 'number,body' \
| jq -c --argjson pr "${{ github.event.pull_request.number }}" '
reduce ( .[] | select(
.body |
scan("(?:blocked (?:by|on)|stacked on):? #([0-9]+)") |
map(tonumber) |
any(.[]; . == $pr)
)) as $i ([]; . + [$i])
'
)
{
echo "deps=$blocked_prs"
echo "numdeps=$(jq -r '. | length' <<< "$blocked_prs")"
} >> "$GITHUB_OUTPUT"
- name: Trigger Blocked PR Workflows for Dependants
if: fromJSON(steps.gather_deps.outputs.numdeps) > 0
env:
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
DEPS: ${{ steps.gather_deps.outputs.deps }}
run: |
while read -r pr ; do
gh -R ${{ github.repository }} workflow run 'blocked-prs.yml' -r "${{ github.ref_name }}" -f pr_id="$pr"
done < <(jq -c '.[].number' <<< "$DEPS")

View file

@ -2,19 +2,30 @@ name: Nix
on: on:
push: push:
tags:
- "*"
paths-ignore: paths-ignore:
- ".github/**"
- "!.github/workflows/nix.yml"
- "flatpak/"
- "scripts/"
- ".git*"
- ".envrc"
- "**.md" - "**.md"
- "**/LICENSE" - "!COPYING.md"
- ".github/ISSUE_TEMPLATE/**" - "renovate.json"
- ".markdownlint**"
- "flatpak/**"
pull_request_target: pull_request_target:
paths-ignore: paths-ignore:
- ".github/**"
- "flatpak/"
- "scripts/"
- ".git*"
- ".envrc"
- "**.md" - "**.md"
- "**/LICENSE" - "!COPYING.md"
- ".github/ISSUE_TEMPLATE/**" - "renovate.json"
- ".markdownlint**"
- "flatpak/**"
workflow_dispatch: workflow_dispatch:
permissions: permissions:
@ -60,7 +71,7 @@ jobs:
# For PRs # For PRs
- name: Setup Nix Magic Cache - name: Setup Nix Magic Cache
if: ${{ env.USE_DETERMINATE }} if: ${{ env.USE_DETERMINATE == 'true' }}
uses: DeterminateSystems/flakehub-cache-action@v1 uses: DeterminateSystems/flakehub-cache-action@v1
# For in-tree builds # For in-tree builds
@ -76,15 +87,18 @@ jobs:
nix flake check --print-build-logs --show-trace nix flake check --print-build-logs --show-trace
- name: Build debug package - name: Build debug package
if: ${{ env.DEBUG }} if: ${{ env.DEBUG == 'true' }}
run: | run: |
nix build \ nix build \
--no-link --print-build-logs --print-out-paths \ --no-link --print-build-logs --print-out-paths \
.#prismlauncher-debug >> "$GITHUB_STEP_SUMMARY" .#prismlauncher-debug >> "$GITHUB_STEP_SUMMARY"
- name: Build release package - name: Build release package
if: ${{ !env.DEBUG }} if: ${{ env.DEBUG == 'false' }}
env:
TAG: ${{ github.ref_name }}
SYSTEM: ${{ matrix.system }}
run: | run: |
nix build \ nix build --no-link --print-out-paths .#prismlauncher \
--no-link --print-build-logs --print-out-paths \ | tee -a "$GITHUB_STEP_SUMMARY" \
.#prismlauncher >> "$GITHUB_STEP_SUMMARY" | xargs cachix pin prismlauncher "$TAG"-"$SYSTEM"

View file

@ -8,28 +8,6 @@ permissions:
contents: read contents: read
jobs: jobs:
flakehub:
name: FlakeHub
runs-on: ubuntu-latest
permissions:
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ github.ref }}
- name: Install Nix
uses: cachix/install-nix-action@v31
- name: Publish on FlakeHub
uses: determinatesystems/flakehub-push@v5
with:
visibility: "public"
winget: winget:
name: Winget name: Winget

View file

@ -49,7 +49,6 @@ jobs:
mv PrismLauncher-Linux-Qt5-Portable*/PrismLauncher-portable.tar.gz PrismLauncher-Linux-Qt5-Portable-${{ env.VERSION }}.tar.gz mv PrismLauncher-Linux-Qt5-Portable*/PrismLauncher-portable.tar.gz PrismLauncher-Linux-Qt5-Portable-${{ env.VERSION }}.tar.gz
mv PrismLauncher-*.AppImage/PrismLauncher-*.AppImage PrismLauncher-Linux-x86_64.AppImage mv PrismLauncher-*.AppImage/PrismLauncher-*.AppImage PrismLauncher-Linux-x86_64.AppImage
mv PrismLauncher-*.AppImage.zsync/PrismLauncher-*.AppImage.zsync PrismLauncher-Linux-x86_64.AppImage.zsync mv PrismLauncher-*.AppImage.zsync/PrismLauncher-*.AppImage.zsync PrismLauncher-Linux-x86_64.AppImage.zsync
mv PrismLauncher-macOS-Legacy*/PrismLauncher.zip PrismLauncher-macOS-Legacy-${{ env.VERSION }}.zip
mv PrismLauncher-macOS*/PrismLauncher.zip PrismLauncher-macOS-${{ env.VERSION }}.zip mv PrismLauncher-macOS*/PrismLauncher.zip PrismLauncher-macOS-${{ env.VERSION }}.zip
tar --exclude='.git' -czf PrismLauncher-${{ env.VERSION }}.tar.gz PrismLauncher-${{ env.VERSION }} tar --exclude='.git' -czf PrismLauncher-${{ env.VERSION }}.tar.gz PrismLauncher-${{ env.VERSION }}
@ -104,5 +103,4 @@ jobs:
PrismLauncher-Windows-MSVC-Portable-${{ env.VERSION }}.zip PrismLauncher-Windows-MSVC-Portable-${{ env.VERSION }}.zip
PrismLauncher-Windows-MSVC-Setup-${{ env.VERSION }}.exe PrismLauncher-Windows-MSVC-Setup-${{ env.VERSION }}.exe
PrismLauncher-macOS-${{ env.VERSION }}.zip PrismLauncher-macOS-${{ env.VERSION }}.zip
PrismLauncher-macOS-Legacy-${{ env.VERSION }}.zip
PrismLauncher-${{ env.VERSION }}.tar.gz PrismLauncher-${{ env.VERSION }}.tar.gz

View file

@ -17,7 +17,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f # v31 - uses: cachix/install-nix-action@d1ca217b388ee87b2507a9a93bf01368bde7cec2 # v31
- uses: DeterminateSystems/update-flake-lock@v24 - uses: DeterminateSystems/update-flake-lock@v24
with: with:

7
.gitignore vendored
View file

@ -21,6 +21,7 @@ CMakeCache.txt
/.vs /.vs
cmake-build-*/ cmake-build-*/
Debug Debug
compile_commands.json
# Build dirs # Build dirs
build build
@ -47,8 +48,12 @@ run/
# Nix/NixOS # Nix/NixOS
.direnv/ .direnv/
.pre-commit-config.yaml ## Used when manually invoking stdenv phases
outputs/
## Regular artifacts
result result
result-*
repl-result-*
# Flatpak # Flatpak
.flatpak-builder .flatpak-builder

3
.gitmodules vendored
View file

@ -4,9 +4,6 @@
[submodule "libraries/tomlplusplus"] [submodule "libraries/tomlplusplus"]
path = libraries/tomlplusplus path = libraries/tomlplusplus
url = https://github.com/marzer/tomlplusplus.git url = https://github.com/marzer/tomlplusplus.git
[submodule "libraries/filesystem"]
path = libraries/filesystem
url = https://github.com/gulrak/filesystem
[submodule "libraries/libnbtplusplus"] [submodule "libraries/libnbtplusplus"]
path = libraries/libnbtplusplus path = libraries/libnbtplusplus
url = https://github.com/PrismLauncher/libnbtplusplus.git url = https://github.com/PrismLauncher/libnbtplusplus.git

View file

@ -99,6 +99,12 @@ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DTOML_ENABLE_FLOAT16=0")
# set CXXFLAGS for build targets # set CXXFLAGS for build targets
set(CMAKE_CXX_FLAGS_RELEASE "-O2 -D_FORTIFY_SOURCE=2 ${CMAKE_CXX_FLAGS_RELEASE}") set(CMAKE_CXX_FLAGS_RELEASE "-O2 -D_FORTIFY_SOURCE=2 ${CMAKE_CXX_FLAGS_RELEASE}")
# 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")
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
endif()
option(DEBUG_ADDRESS_SANITIZER "Enable Address Sanitizer in Debug builds" OFF) option(DEBUG_ADDRESS_SANITIZER "Enable Address Sanitizer in Debug builds" OFF)
# If this is a Debug build turn on address sanitiser # If this is a Debug build turn on address sanitiser
@ -189,10 +195,11 @@ set(Launcher_FMLLIBS_BASE_URL "https://files.prismlauncher.org/fmllibs/" CACHE S
######## Set version numbers ######## ######## Set version numbers ########
set(Launcher_VERSION_MAJOR 10) set(Launcher_VERSION_MAJOR 10)
set(Launcher_VERSION_MINOR 0) set(Launcher_VERSION_MINOR 0)
set(Launcher_VERSION_PATCH 0)
set(Launcher_VERSION_NAME "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}") set(Launcher_VERSION_NAME "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_PATCH}")
set(Launcher_VERSION_NAME4 "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.0.0") set(Launcher_VERSION_NAME4 "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_PATCH}.0")
set(Launcher_VERSION_NAME4_COMMA "${Launcher_VERSION_MAJOR},${Launcher_VERSION_MINOR},0,0") set(Launcher_VERSION_NAME4_COMMA "${Launcher_VERSION_MAJOR},${Launcher_VERSION_MINOR},${Launcher_VERSION_PATCH},0")
# Build platform. # Build platform.
set(Launcher_BUILD_PLATFORM "unknown" CACHE STRING "A short string identifying the platform that this build was built for. Only used to display in the about dialog.") set(Launcher_BUILD_PLATFORM "unknown" CACHE STRING "A short string identifying the platform that this build was built for. Only used to display in the about dialog.")
@ -357,9 +364,6 @@ if(NOT Launcher_FORCE_BUNDLED_LIBS)
# Find toml++ # Find toml++
find_package(tomlplusplus 3.2.0 QUIET) find_package(tomlplusplus 3.2.0 QUIET)
# Find ghc_filesystem
find_package(ghc_filesystem QUIET)
# Find cmark # Find cmark
find_package(cmark QUIET) find_package(cmark QUIET)
endif() endif()
@ -554,12 +558,6 @@ else()
endif() endif()
add_subdirectory(libraries/gamemode) add_subdirectory(libraries/gamemode)
add_subdirectory(libraries/murmur2) # Hash for usage with the CurseForge API add_subdirectory(libraries/murmur2) # Hash for usage with the CurseForge API
if (NOT ghc_filesystem_FOUND)
message(STATUS "Using bundled ghc_filesystem")
add_subdirectory(libraries/filesystem) # Implementation of std::filesystem for old C++, for usage in old macOS
else()
message(STATUS "Using system ghc_filesystem")
endif()
add_subdirectory(libraries/qdcss) # css parser add_subdirectory(libraries/qdcss) # css parser
############################### Built Artifacts ############################### ############################### Built Artifacts ###############################

View file

@ -362,28 +362,6 @@
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE. POSSIBILITY OF SUCH DAMAGE.
## gulrak/filesystem
Copyright (c) 2018, Steffen Schümann <s.schuemann@pobox.com>
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.
## Breeze icons ## Breeze icons
Copyright (C) 2014 Uri Herrera <uri_herrera@nitrux.in> and others Copyright (C) 2014 Uri Herrera <uri_herrera@nitrux.in> and others

View file

@ -34,8 +34,8 @@
*/ */
#include <qstringliteral.h> #include <qstringliteral.h>
#include "BuildConfig.h"
#include <QObject> #include <QObject>
#include "BuildConfig.h"
const Config BuildConfig; const Config BuildConfig;
@ -58,6 +58,7 @@ Config::Config()
// Version information // Version information
VERSION_MAJOR = @Launcher_VERSION_MAJOR@; VERSION_MAJOR = @Launcher_VERSION_MAJOR@;
VERSION_MINOR = @Launcher_VERSION_MINOR@; VERSION_MINOR = @Launcher_VERSION_MINOR@;
VERSION_PATCH = @Launcher_VERSION_PATCH@;
BUILD_PLATFORM = "@Launcher_BUILD_PLATFORM@"; BUILD_PLATFORM = "@Launcher_BUILD_PLATFORM@";
BUILD_ARTIFACT = "@Launcher_BUILD_ARTIFACT@"; BUILD_ARTIFACT = "@Launcher_BUILD_ARTIFACT@";
@ -74,8 +75,7 @@ Config::Config()
MAC_SPARKLE_PUB_KEY = "@MACOSX_SPARKLE_UPDATE_PUBLIC_KEY@"; MAC_SPARKLE_PUB_KEY = "@MACOSX_SPARKLE_UPDATE_PUBLIC_KEY@";
MAC_SPARKLE_APPCAST_URL = "@MACOSX_SPARKLE_UPDATE_FEED_URL@"; MAC_SPARKLE_APPCAST_URL = "@MACOSX_SPARKLE_UPDATE_FEED_URL@";
if (!MAC_SPARKLE_PUB_KEY.isEmpty() && !MAC_SPARKLE_APPCAST_URL.isEmpty()) if (!MAC_SPARKLE_PUB_KEY.isEmpty() && !MAC_SPARKLE_APPCAST_URL.isEmpty()) {
{
UPDATER_ENABLED = true; UPDATER_ENABLED = true;
} else if (!UPDATER_GITHUB_REPO.isEmpty() && !BUILD_ARTIFACT.isEmpty()) { } else if (!UPDATER_GITHUB_REPO.isEmpty() && !BUILD_ARTIFACT.isEmpty()) {
UPDATER_ENABLED = true; UPDATER_ENABLED = true;
@ -89,27 +89,19 @@ Config::Config()
GIT_REFSPEC = "@Launcher_GIT_REFSPEC@"; GIT_REFSPEC = "@Launcher_GIT_REFSPEC@";
// Assume that builds outside of Git repos are "stable" // Assume that builds outside of Git repos are "stable"
if (GIT_REFSPEC == QStringLiteral("GITDIR-NOTFOUND") if (GIT_REFSPEC == QStringLiteral("GITDIR-NOTFOUND") || GIT_TAG == QStringLiteral("GITDIR-NOTFOUND") ||
|| GIT_TAG == QStringLiteral("GITDIR-NOTFOUND") GIT_REFSPEC == QStringLiteral("") || GIT_TAG == QStringLiteral("GIT-NOTFOUND")) {
|| GIT_REFSPEC == QStringLiteral("")
|| GIT_TAG == QStringLiteral("GIT-NOTFOUND"))
{
GIT_REFSPEC = "refs/heads/stable"; GIT_REFSPEC = "refs/heads/stable";
GIT_TAG = versionString(); GIT_TAG = versionString();
GIT_COMMIT = ""; GIT_COMMIT = "";
} }
if (GIT_REFSPEC.startsWith("refs/heads/")) if (GIT_REFSPEC.startsWith("refs/heads/")) {
{
VERSION_CHANNEL = GIT_REFSPEC; VERSION_CHANNEL = GIT_REFSPEC;
VERSION_CHANNEL.remove("refs/heads/"); VERSION_CHANNEL.remove("refs/heads/");
} } else if (!GIT_COMMIT.isEmpty()) {
else if (!GIT_COMMIT.isEmpty())
{
VERSION_CHANNEL = GIT_COMMIT.mid(0, 8); VERSION_CHANNEL = GIT_COMMIT.mid(0, 8);
} } else {
else
{
VERSION_CHANNEL = "unknown"; VERSION_CHANNEL = "unknown";
} }
@ -136,7 +128,7 @@ Config::Config()
QString Config::versionString() const QString Config::versionString() const
{ {
return QString("%1.%2").arg(VERSION_MAJOR).arg(VERSION_MINOR); return QString("%1.%2.%3").arg(VERSION_MAJOR).arg(VERSION_MINOR).arg(VERSION_PATCH);
} }
QString Config::printableVersionString() const QString Config::printableVersionString() const
@ -144,8 +136,7 @@ QString Config::printableVersionString() const
QString vstr = versionString(); QString vstr = versionString();
// If the build is not a main release, append the channel // If the build is not a main release, append the channel
if(VERSION_CHANNEL != "stable" && GIT_TAG != vstr) if (VERSION_CHANNEL != "stable" && GIT_TAG != vstr) {
{
vstr += "-" + VERSION_CHANNEL; vstr += "-" + VERSION_CHANNEL;
} }
return vstr; return vstr;
@ -162,4 +153,3 @@ QString Config::systemID() const
{ {
return QStringLiteral("%1 %2 %3").arg(COMPILER_TARGET_SYSTEM, COMPILER_TARGET_SYSTEM_VERSION, COMPILER_TARGET_SYSTEM_PROCESSOR); return QStringLiteral("%1 %2 %3").arg(COMPILER_TARGET_SYSTEM, COMPILER_TARGET_SYSTEM_VERSION, COMPILER_TARGET_SYSTEM_PROCESSOR);
} }

View file

@ -59,6 +59,8 @@ class Config {
int VERSION_MAJOR; int VERSION_MAJOR;
/// The minor version number. /// The minor version number.
int VERSION_MINOR; int VERSION_MINOR;
/// The patch version number.
int VERSION_PATCH;
/** /**
* The version channel * The version channel

View file

@ -1,9 +1,4 @@
(import ( (import (fetchTarball {
let url = "https://github.com/edolstra/flake-compat/archive/ff81ac966bb2cae68946d5ed5fc4994f96d0ffec.tar.gz";
lock = builtins.fromJSON (builtins.readFile ./flake.lock); sha256 = "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=";
in }) { src = ./.; }).defaultNix
fetchTarball {
url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz";
sha256 = lock.nodes.flake-compat.locked.narHash;
}
) { src = ./.; }).defaultNix

39
flake.lock generated
View file

@ -1,21 +1,5 @@
{ {
"nodes": { "nodes": {
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1733328505,
"narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"libnbtplusplus": { "libnbtplusplus": {
"flake": false, "flake": false,
"locked": { "locked": {
@ -32,28 +16,13 @@
"type": "github" "type": "github"
} }
}, },
"nix-filter": {
"locked": {
"lastModified": 1731533336,
"narHash": "sha256-oRam5PS1vcrr5UPgALW0eo1m/5/pls27Z/pabHNy2Ms=",
"owner": "numtide",
"repo": "nix-filter",
"rev": "f7653272fd234696ae94229839a99b73c9ab7de0",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "nix-filter",
"type": "github"
}
},
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1741851582, "lastModified": 1744463964,
"narHash": "sha256-cPfs8qMccim2RBgtKGF+x9IBCduRvd/N5F4nYpU0TVE=", "narHash": "sha256-LWqduOgLHCFxiTNYi3Uj5Lgz0SR+Xhw3kr/3Xd0GPTM=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "6607cf789e541e7873d40d3a8f7815ea92204f32", "rev": "2631b0b7abcea6e640ce31cd78ea58910d31e650",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -65,9 +34,7 @@
}, },
"root": { "root": {
"inputs": { "inputs": {
"flake-compat": "flake-compat",
"libnbtplusplus": "libnbtplusplus", "libnbtplusplus": "libnbtplusplus",
"nix-filter": "nix-filter",
"nixpkgs": "nixpkgs" "nixpkgs": "nixpkgs"
} }
} }

151
flake.nix
View file

@ -15,28 +15,6 @@
url = "github:PrismLauncher/libnbtplusplus"; url = "github:PrismLauncher/libnbtplusplus";
flake = false; flake = false;
}; };
nix-filter.url = "github:numtide/nix-filter";
/*
Inputs below this are optional and can be removed
```
{
inputs.prismlauncher = {
url = "github:PrismLauncher/PrismLauncher";
inputs = {
flake-compat.follows = "";
};
};
}
```
*/
flake-compat = {
url = "github:edolstra/flake-compat";
flake = false;
};
}; };
outputs = outputs =
@ -44,9 +22,8 @@
self, self,
nixpkgs, nixpkgs,
libnbtplusplus, libnbtplusplus,
nix-filter,
...
}: }:
let let
inherit (nixpkgs) lib; inherit (nixpkgs) lib;
@ -58,27 +35,128 @@
forAllSystems = lib.genAttrs systems; forAllSystems = lib.genAttrs systems;
nixpkgsFor = forAllSystems (system: nixpkgs.legacyPackages.${system}); nixpkgsFor = forAllSystems (system: nixpkgs.legacyPackages.${system});
in in
{ {
checks = forAllSystems ( checks = forAllSystems (
system: system:
let let
checks' = nixpkgsFor.${system}.callPackage ./nix/checks.nix { inherit self; }; pkgs = nixpkgsFor.${system};
llvm = pkgs.llvmPackages_19;
in in
lib.filterAttrs (_: lib.isDerivation) checks'
{
formatting =
pkgs.runCommand "check-formatting"
{
nativeBuildInputs = with pkgs; [
deadnix
llvm.clang-tools
markdownlint-cli
nixfmt-rfc-style
statix
];
}
''
cd ${self}
echo "Running clang-format...."
clang-format --dry-run --style='file' --Werror */**.{c,cc,cpp,h,hh,hpp}
echo "Running deadnix..."
deadnix --fail
echo "Running markdownlint..."
markdownlint --dot .
echo "Running nixfmt..."
find -type f -name '*.nix' -exec nixfmt --check {} +
echo "Running statix"
statix check .
touch $out
'';
}
); );
devShells = forAllSystems ( devShells = forAllSystems (
system: system:
let let
pkgs = nixpkgsFor.${system}; pkgs = nixpkgsFor.${system};
llvm = pkgs.llvmPackages_19;
packages' = self.packages.${system};
welcomeMessage = ''
Welcome to the Prism Launcher repository! 🌈
We just set some things up for you. To get building, you can run:
```
$ cd "$cmakeBuildDir"
$ ninjaBuildPhase
$ ninjaInstallPhase
```
Feel free to ask any questions in our Discord server or Matrix space:
- https://prismlauncher.org/discord
- https://matrix.to/#/#prismlauncher:matrix.org
And thanks for helping out :)
'';
# Re-use our package wrapper to wrap our development environment
qt-wrapper-env = packages'.prismlauncher.overrideAttrs (old: {
name = "qt-wrapper-env";
# Required to use script-based makeWrapper below
strictDeps = true;
# We don't need/want the unwrapped Prism package
paths = [ ];
nativeBuildInputs = old.nativeBuildInputs or [ ] ++ [
# Ensure the wrapper is script based so it can be sourced
pkgs.makeWrapper
];
# Inspired by https://discourse.nixos.org/t/python-qt-woes/11808/10
buildCommand = ''
makeQtWrapper ${lib.getExe pkgs.runtimeShellPackage} "$out"
sed -i '/^exec/d' "$out"
'';
});
in in
{ {
default = pkgs.mkShell { default = pkgs.mkShell {
inputsFrom = [ self.packages.${system}.prismlauncher-unwrapped ]; inputsFrom = [ packages'.prismlauncher-unwrapped ];
buildInputs = with pkgs; [
packages = with pkgs; [
ccache ccache
ninja llvm.clang-tools
]; ];
cmakeBuildType = "Debug";
cmakeFlags = [ "-GNinja" ] ++ packages'.prismlauncher.cmakeFlags;
dontFixCmake = true;
shellHook = ''
echo "Sourcing ${qt-wrapper-env}"
source ${qt-wrapper-env}
git submodule update --init --force
if [ ! -f compile_commands.json ]; then
cmakeConfigurePhase
cd ..
ln -s "$cmakeBuildDir"/compile_commands.json compile_commands.json
fi
echo ${lib.escapeShellArg welcomeMessage}
'';
}; };
} }
); );
@ -89,7 +167,6 @@
prismlauncher-unwrapped = prev.callPackage ./nix/unwrapped.nix { prismlauncher-unwrapped = prev.callPackage ./nix/unwrapped.nix {
inherit inherit
libnbtplusplus libnbtplusplus
nix-filter
self self
; ;
}; };
@ -99,6 +176,7 @@
packages = forAllSystems ( packages = forAllSystems (
system: system:
let let
pkgs = nixpkgsFor.${system}; pkgs = nixpkgsFor.${system};
@ -111,6 +189,7 @@
default = prismPackages.prismlauncher; default = prismPackages.prismlauncher;
}; };
in in
# Only output them if they're available on the current system # Only output them if they're available on the current system
lib.filterAttrs (_: lib.meta.availableOn pkgs.stdenv.hostPlatform) packages lib.filterAttrs (_: lib.meta.availableOn pkgs.stdenv.hostPlatform) packages
); );
@ -118,16 +197,18 @@
# We put these under legacyPackages as they are meant for CI, not end user consumption # We put these under legacyPackages as they are meant for CI, not end user consumption
legacyPackages = forAllSystems ( legacyPackages = forAllSystems (
system: system:
let let
prismPackages = self.packages.${system}; packages' = self.packages.${system};
legacyPackages = self.legacyPackages.${system}; legacyPackages' = self.legacyPackages.${system};
in in
{ {
prismlauncher-debug = prismPackages.prismlauncher.override { prismlauncher-debug = packages'.prismlauncher.override {
prismlauncher-unwrapped = legacyPackages.prismlauncher-unwrapped-debug; prismlauncher-unwrapped = legacyPackages'.prismlauncher-unwrapped-debug;
}; };
prismlauncher-unwrapped-debug = prismPackages.prismlauncher-unwrapped.overrideAttrs { prismlauncher-unwrapped-debug = packages'.prismlauncher-unwrapped.overrideAttrs {
cmakeBuildType = "Debug"; cmakeBuildType = "Debug";
dontStrip = true; dontStrip = true;
}; };

View file

@ -376,19 +376,20 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_peerInstance = new LocalPeer(this, appID); m_peerInstance = new LocalPeer(this, appID);
connect(m_peerInstance, &LocalPeer::messageReceived, this, &Application::messageReceived); connect(m_peerInstance, &LocalPeer::messageReceived, this, &Application::messageReceived);
if (m_peerInstance->isClient()) { if (m_peerInstance->isClient()) {
bool sentMessage = false;
int timeout = 2000; int timeout = 2000;
if (m_instanceIdToLaunch.isEmpty()) { if (m_instanceIdToLaunch.isEmpty()) {
ApplicationMessage activate; ApplicationMessage activate;
activate.command = "activate"; activate.command = "activate";
m_peerInstance->sendMessage(activate.serialize(), timeout); sentMessage = m_peerInstance->sendMessage(activate.serialize(), timeout);
if (!m_urlsToImport.isEmpty()) { if (!m_urlsToImport.isEmpty()) {
for (auto url : m_urlsToImport) { for (auto url : m_urlsToImport) {
ApplicationMessage import; ApplicationMessage import;
import.command = "import"; import.command = "import";
import.args.insert("url", url.toString()); import.args.insert("url", url.toString());
m_peerInstance->sendMessage(import.serialize(), timeout); sentMessage = m_peerInstance->sendMessage(import.serialize(), timeout);
} }
} }
} else { } else {
@ -408,10 +409,16 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
launch.args["offline_enabled"] = "true"; launch.args["offline_enabled"] = "true";
launch.args["offline_name"] = m_offlineName; launch.args["offline_name"] = m_offlineName;
} }
m_peerInstance->sendMessage(launch.serialize(), timeout); sentMessage = m_peerInstance->sendMessage(launch.serialize(), timeout);
} }
if (sentMessage) {
m_status = Application::Succeeded; m_status = Application::Succeeded;
return; return;
} else {
std::cerr << "Unable to redirect command to already running instance\n";
// C function not Qt function - event loop not started yet
::exit(1);
}
} }
} }
@ -710,7 +717,9 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_settings->registerSetting("ToolbarsLocked", false); m_settings->registerSetting("ToolbarsLocked", false);
// Instance
m_settings->registerSetting("InstSortMode", "Name"); m_settings->registerSetting("InstSortMode", "Name");
m_settings->registerSetting("InstRenamingMode", "AskEverytime");
m_settings->registerSetting("SelectedInstance", QString()); m_settings->registerSetting("SelectedInstance", QString());
// Window state and geometry // Window state and geometry

View file

@ -386,6 +386,12 @@ void BaseInstance::setName(QString val)
emit propertiesChanged(this); emit propertiesChanged(this);
} }
bool BaseInstance::syncInstanceDirName(const QString& newRoot) const
{
auto oldRoot = instanceRoot();
return oldRoot == newRoot || QFile::rename(oldRoot, newRoot);
}
QString BaseInstance::name() const QString BaseInstance::name() const
{ {
return m_settings->get("name").toString(); return m_settings->get("name").toString();

View file

@ -126,6 +126,9 @@ class BaseInstance : public QObject, public std::enable_shared_from_this<BaseIns
QString name() const; QString name() const;
void setName(QString val); void setName(QString val);
/// Sync name and rename instance dir accordingly; returns true if successful
bool syncInstanceDirName(const QString& newRoot) const;
/// Value used for instance window titles /// Value used for instance window titles
QString windowTitle() const; QString windowTitle() const;

View file

@ -21,6 +21,8 @@ set(CORE_SOURCES
BaseVersion.h BaseVersion.h
BaseInstance.h BaseInstance.h
BaseInstance.cpp BaseInstance.cpp
InstanceDirUpdate.h
InstanceDirUpdate.cpp
NullInstance.h NullInstance.h
MMCZip.h MMCZip.h
MMCZip.cpp MMCZip.cpp
@ -1286,7 +1288,6 @@ target_link_libraries(Launcher_logic
qdcss qdcss
BuildConfig BuildConfig
Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Widgets
ghcFilesystem::ghc_filesystem
) )
if (UNIX AND NOT CYGWIN AND NOT APPLE) if (UNIX AND NOT CYGWIN AND NOT APPLE)
@ -1373,7 +1374,6 @@ if(Launcher_BUILD_UPDATER)
${ZLIB_LIBRARIES} ${ZLIB_LIBRARIES}
systeminfo systeminfo
BuildConfig BuildConfig
ghcFilesystem::ghc_filesystem
Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Widgets
Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Core
Qt${QT_VERSION_MAJOR}::Network Qt${QT_VERSION_MAJOR}::Network
@ -1412,7 +1412,6 @@ if(WIN32 OR (DEFINED Launcher_BUILD_FILELINKER AND Launcher_BUILD_FILELINKER))
target_link_libraries(filelink_logic target_link_libraries(filelink_logic
systeminfo systeminfo
BuildConfig BuildConfig
ghcFilesystem::ghc_filesystem
Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Widgets
Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Core
Qt${QT_VERSION_MAJOR}::Network Qt${QT_VERSION_MAJOR}::Network

View file

@ -15,7 +15,7 @@
DataMigrationTask::DataMigrationTask(const QString& sourcePath, const QString& targetPath, const IPathMatcher::Ptr pathMatcher) DataMigrationTask::DataMigrationTask(const QString& sourcePath, const QString& targetPath, const IPathMatcher::Ptr pathMatcher)
: Task(), m_sourcePath(sourcePath), m_targetPath(targetPath), m_pathMatcher(pathMatcher), m_copy(sourcePath, targetPath) : Task(), m_sourcePath(sourcePath), m_targetPath(targetPath), m_pathMatcher(pathMatcher), m_copy(sourcePath, targetPath)
{ {
m_copy.matcher(m_pathMatcher.get()).whitelist(true); m_copy.matcher(m_pathMatcher).whitelist(true);
} }
void DataMigrationTask::executeTask() void DataMigrationTask::executeTask()

View file

@ -77,24 +77,8 @@
#include <utime.h> #include <utime.h>
#endif #endif
// Snippet from https://github.com/gulrak/filesystem#using-it-as-single-file-header
#ifdef __APPLE__
#include <Availability.h> // for deployment target to support pre-catalina targets without std::fs
#endif // __APPLE__
#if ((defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) || (defined(__cplusplus) && __cplusplus >= 201703L)) && defined(__has_include)
#if __has_include(<filesystem>) && (!defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || __MAC_OS_X_VERSION_MIN_REQUIRED >= 101500)
#define GHC_USE_STD_FS
#include <filesystem> #include <filesystem>
namespace fs = std::filesystem; namespace fs = std::filesystem;
#endif // MacOS min version check
#endif // Other OSes version check
#ifndef GHC_USE_STD_FS
#include <ghc/filesystem.hpp>
namespace fs = ghc::filesystem;
#endif
// clone // clone
#if defined(Q_OS_LINUX) #if defined(Q_OS_LINUX)
@ -950,7 +934,7 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri
QDir content = application.path() + "/Contents/"; QDir content = application.path() + "/Contents/";
QDir resources = content.path() + "/Resources/"; QDir resources = content.path() + "/Resources/";
QDir binaryDir = content.path() + "/MacOS/"; QDir binaryDir = content.path() + "/MacOS/";
QFile info = content.path() + "/Info.plist"; QFile info(content.path() + "/Info.plist");
if (!(content.mkpath(".") && resources.mkpath(".") && binaryDir.mkpath("."))) { if (!(content.mkpath(".") && resources.mkpath(".") && binaryDir.mkpath("."))) {
qWarning() << "Couldn't create directories within application"; qWarning() << "Couldn't create directories within application";

View file

@ -115,7 +115,7 @@ class copy : public QObject {
m_followSymlinks = follow; m_followSymlinks = follow;
return *this; return *this;
} }
copy& matcher(const IPathMatcher* filter) copy& matcher(IPathMatcher::Ptr filter)
{ {
m_matcher = filter; m_matcher = filter;
return *this; return *this;
@ -147,7 +147,7 @@ class copy : public QObject {
private: private:
bool m_followSymlinks = true; bool m_followSymlinks = true;
const IPathMatcher* m_matcher = nullptr; IPathMatcher::Ptr m_matcher = nullptr;
bool m_whitelist = false; bool m_whitelist = false;
bool m_overwrite = false; bool m_overwrite = false;
QDir m_src; QDir m_src;
@ -209,7 +209,7 @@ class create_link : public QObject {
m_useHardLinks = useHard; m_useHardLinks = useHard;
return *this; return *this;
} }
create_link& matcher(const IPathMatcher* filter) create_link& matcher(IPathMatcher::Ptr filter)
{ {
m_matcher = filter; m_matcher = filter;
return *this; return *this;
@ -260,7 +260,7 @@ class create_link : public QObject {
private: private:
bool m_useHardLinks = false; bool m_useHardLinks = false;
const IPathMatcher* m_matcher = nullptr; IPathMatcher::Ptr m_matcher = nullptr;
bool m_whitelist = false; bool m_whitelist = false;
bool m_recursive = true; bool m_recursive = true;
@ -488,7 +488,7 @@ class clone : public QObject {
m_src.setPath(src); m_src.setPath(src);
m_dst.setPath(dst); m_dst.setPath(dst);
} }
clone& matcher(const IPathMatcher* filter) clone& matcher(IPathMatcher::Ptr filter)
{ {
m_matcher = filter; m_matcher = filter;
return *this; return *this;
@ -514,7 +514,7 @@ class clone : public QObject {
bool operator()(const QString& offset, bool dryRun = false); bool operator()(const QString& offset, bool dryRun = false);
private: private:
const IPathMatcher* m_matcher = nullptr; IPathMatcher::Ptr m_matcher = nullptr;
bool m_whitelist = false; bool m_whitelist = false;
QDir m_src; QDir m_src;
QDir m_dst; QDir m_dst;

View file

@ -43,7 +43,7 @@ void InstanceCopyTask::executeTask()
m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this] { m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this] {
if (m_useClone) { if (m_useClone) {
FS::clone folderClone(m_origInstance->instanceRoot(), m_stagingPath); FS::clone folderClone(m_origInstance->instanceRoot(), m_stagingPath);
folderClone.matcher(m_matcher.get()); folderClone.matcher(m_matcher);
folderClone(true); folderClone(true);
setProgress(0, folderClone.totalCloned()); setProgress(0, folderClone.totalCloned());
@ -72,7 +72,7 @@ void InstanceCopyTask::executeTask()
} }
FS::create_link folderLink(m_origInstance->instanceRoot(), m_stagingPath); FS::create_link folderLink(m_origInstance->instanceRoot(), m_stagingPath);
int depth = m_linkRecursively ? -1 : 0; // we need to at least link the top level instead of the instance folder int depth = m_linkRecursively ? -1 : 0; // we need to at least link the top level instead of the instance folder
folderLink.linkRecursively(true).setMaxDepth(depth).useHardLinks(m_useHardLinks).matcher(m_matcher.get()); folderLink.linkRecursively(true).setMaxDepth(depth).useHardLinks(m_useHardLinks).matcher(m_matcher);
folderLink(true); folderLink(true);
setProgress(0, m_progressTotal + folderLink.totalToLink()); setProgress(0, m_progressTotal + folderLink.totalToLink());
@ -127,7 +127,7 @@ void InstanceCopyTask::executeTask()
return !there_were_errors; return !there_were_errors;
} }
FS::copy folderCopy(m_origInstance->instanceRoot(), m_stagingPath); FS::copy folderCopy(m_origInstance->instanceRoot(), m_stagingPath);
folderCopy.followSymlinks(false).matcher(m_matcher.get()); folderCopy.followSymlinks(false).matcher(m_matcher);
folderCopy(true); folderCopy(true);
setProgress(0, folderCopy.totalCopied()); setProgress(0, folderCopy.totalCopied());

View file

@ -28,7 +28,7 @@ class InstanceCopyTask : public InstanceTask {
InstancePtr m_origInstance; InstancePtr m_origInstance;
QFuture<bool> m_copyFuture; QFuture<bool> m_copyFuture;
QFutureWatcher<bool> m_copyFutureWatcher; QFutureWatcher<bool> m_copyFutureWatcher;
std::unique_ptr<IPathMatcher> m_matcher; IPathMatcher::Ptr m_matcher;
bool m_keepPlaytime; bool m_keepPlaytime;
bool m_useLinks = false; bool m_useLinks = false;
bool m_useHardLinks = false; bool m_useHardLinks = false;

View file

@ -0,0 +1,126 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "InstanceDirUpdate.h"
#include <QCheckBox>
#include "Application.h"
#include "FileSystem.h"
#include "InstanceList.h"
#include "ui/dialogs/CustomMessageBox.h"
QString askToUpdateInstanceDirName(InstancePtr instance, const QString& oldName, const QString& newName, QWidget* parent)
{
if (oldName == newName)
return QString();
QString renamingMode = APPLICATION->settings()->get("InstRenamingMode").toString();
if (renamingMode == "MetadataOnly")
return QString();
auto oldRoot = instance->instanceRoot();
auto newDirName = FS::DirNameFromString(newName, QFileInfo(oldRoot).dir().absolutePath());
auto newRoot = FS::PathCombine(QFileInfo(oldRoot).dir().absolutePath(), newDirName);
if (oldRoot == newRoot)
return QString();
if (oldRoot == FS::PathCombine(QFileInfo(oldRoot).dir().absolutePath(), newName))
return QString();
// Check for conflict
if (QDir(newRoot).exists()) {
QMessageBox::warning(parent, QObject::tr("Cannot rename instance"),
QObject::tr("New instance root (%1) already exists. <br />Only the metadata will be renamed.").arg(newRoot));
return QString();
}
// Ask if we should rename
if (renamingMode == "AskEverytime") {
auto checkBox = new QCheckBox(QObject::tr("&Remember my choice"), parent);
auto dialog =
CustomMessageBox::selectable(parent, QObject::tr("Rename instance folder"),
QObject::tr("Would you also like to rename the instance folder?\n\n"
"Old name: %1\n"
"New name: %2")
.arg(oldName, newName),
QMessageBox::Question, QMessageBox::No | QMessageBox::Yes, QMessageBox::NoButton, checkBox);
auto res = dialog->exec();
if (checkBox->isChecked()) {
if (res == QMessageBox::Yes)
APPLICATION->settings()->set("InstRenamingMode", "PhysicalDir");
else
APPLICATION->settings()->set("InstRenamingMode", "MetadataOnly");
}
if (res == QMessageBox::No)
return QString();
}
// Check for linked instances
if (!checkLinkedInstances(instance->id(), parent, QObject::tr("Renaming")))
return QString();
// Now we can confirm that a renaming is happening
if (!instance->syncInstanceDirName(newRoot)) {
QMessageBox::warning(parent, QObject::tr("Cannot rename instance"),
QObject::tr("An error occurred when performing the following renaming operation: <br/>"
" - Old instance root: %1<br/>"
" - New instance root: %2<br/>"
"Only the metadata is renamed.")
.arg(oldRoot, newRoot));
return QString();
}
return newRoot;
}
bool checkLinkedInstances(const QString& id, QWidget* parent, const QString& verb)
{
auto linkedInstances = APPLICATION->instances()->getLinkedInstancesById(id);
if (!linkedInstances.empty()) {
auto response = CustomMessageBox::selectable(parent, QObject::tr("There are linked instances"),
QObject::tr("The following instance(s) might reference files in this instance:\n\n"
"%1\n\n"
"%2 it could break the other instance(s), \n\n"
"Do you wish to proceed?",
nullptr, linkedInstances.count())
.arg(linkedInstances.join("\n"))
.arg(verb),
QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No)
->exec();
if (response != QMessageBox::Yes)
return false;
}
return true;
}

View file

@ -0,0 +1,43 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "BaseInstance.h"
/// Update instanceRoot to make it sync with name/id; return newRoot if a directory rename happened
QString askToUpdateInstanceDirName(InstancePtr instance, const QString& oldName, const QString& newName, QWidget* parent);
/// Check if there are linked instances, and display a warning; return true if the operation should proceed
bool checkLinkedInstances(const QString& id, QWidget* parent, const QString& verb);

View file

@ -24,7 +24,7 @@ class TestCheck : public QObject {
TestCheck(QWidget* parent, QString path, QString args, int minMem, int maxMem, int permGen) TestCheck(QWidget* parent, QString path, QString args, int minMem, int maxMem, int permGen)
: m_parent(parent), m_path(path), m_args(args), m_minMem(minMem), m_maxMem(maxMem), m_permGen(permGen) : m_parent(parent), m_path(path), m_args(args), m_minMem(minMem), m_maxMem(maxMem), m_permGen(permGen)
{} {}
virtual ~TestCheck() {}; virtual ~TestCheck() = default;
void run(); void run();

View file

@ -193,8 +193,8 @@ QVariant VersionProxyModel::data(const QModelIndex& index, int role) const
if (value.toBool()) { if (value.toBool()) {
return tr("Recommended"); return tr("Recommended");
} else if (hasLatest) { } else if (hasLatest) {
auto value = sourceModel()->data(parentIndex, BaseVersionList::LatestRole); auto latest = sourceModel()->data(parentIndex, BaseVersionList::LatestRole);
if (value.toBool()) { if (latest.toBool()) {
return tr("Latest"); return tr("Latest");
} }
} }
@ -203,9 +203,7 @@ QVariant VersionProxyModel::data(const QModelIndex& index, int role) const
} }
} }
case Qt::DecorationRole: { case Qt::DecorationRole: {
switch (column) { if (column == Name && hasRecommended) {
case Name: {
if (hasRecommended) {
auto recommenced = sourceModel()->data(parentIndex, BaseVersionList::RecommendedRole); auto recommenced = sourceModel()->data(parentIndex, BaseVersionList::RecommendedRole);
if (recommenced.toBool()) { if (recommenced.toBool()) {
return APPLICATION->getThemedIcon("star"); return APPLICATION->getThemedIcon("star");
@ -225,12 +223,8 @@ QVariant VersionProxyModel::data(const QModelIndex& index, int role) const
} }
return pixmap; return pixmap;
} }
}
default: {
return QVariant(); return QVariant();
} }
}
}
default: { default: {
if (roles.contains((BaseVersionList::ModelRoles)role)) { if (roles.contains((BaseVersionList::ModelRoles)role)) {
return sourceModel()->data(parentIndex, role); return sourceModel()->data(parentIndex, role);

View file

@ -40,24 +40,8 @@
#include "WindowsConsole.h" #include "WindowsConsole.h"
#endif #endif
// Snippet from https://github.com/gulrak/filesystem#using-it-as-single-file-header
#ifdef __APPLE__
#include <Availability.h> // for deployment target to support pre-catalina targets without std::fs
#endif // __APPLE__
#if ((defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) || (defined(__cplusplus) && __cplusplus >= 201703L)) && defined(__has_include)
#if __has_include(<filesystem>) && (!defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || __MAC_OS_X_VERSION_MIN_REQUIRED >= 101500)
#define GHC_USE_STD_FS
#include <filesystem> #include <filesystem>
namespace fs = std::filesystem; namespace fs = std::filesystem;
#endif // MacOS min version check
#endif // Other OSes version check
#ifndef GHC_USE_STD_FS
#include <ghc/filesystem.hpp>
namespace fs = ghc::filesystem;
#endif
FileLinkApp::FileLinkApp(int& argc, char** argv) : QCoreApplication(argc, argv), socket(new QLocalSocket(this)) FileLinkApp::FileLinkApp(int& argc, char** argv) : QCoreApplication(argc, argv), socket(new QLocalSocket(this))
{ {

View file

@ -517,14 +517,10 @@ QVariant PackProfile::data(const QModelIndex& index, int role) const
switch (role) { switch (role) {
case Qt::CheckStateRole: { case Qt::CheckStateRole: {
switch (column) { if (column == NameColumn)
case NameColumn: {
return patch->isEnabled() ? Qt::Checked : Qt::Unchecked; return patch->isEnabled() ? Qt::Checked : Qt::Unchecked;
}
default:
return QVariant(); return QVariant();
} }
}
case Qt::DisplayRole: { case Qt::DisplayRole: {
switch (column) { switch (column) {
case NameColumn: case NameColumn:

View file

@ -208,13 +208,9 @@ QVariant WorldList::data(const QModelIndex& index, int role) const
} }
case Qt::UserRole: case Qt::UserRole:
switch (column) { if (column == SizeColumn)
case SizeColumn:
return QVariant::fromValue<qlonglong>(world.bytes()); return QVariant::fromValue<qlonglong>(world.bytes());
default:
return data(index, Qt::DisplayRole); return data(index, Qt::DisplayRole);
}
case Qt::ToolTipRole: { case Qt::ToolTipRole: {
if (column == InfoColumn) { if (column == InfoColumn) {

View file

@ -260,6 +260,30 @@ int AccountList::count() const
return m_accounts.count(); return m_accounts.count();
} }
QString getAccountStatus(AccountState status)
{
switch (status) {
case AccountState::Unchecked:
return QObject::tr("Unchecked", "Account status");
case AccountState::Offline:
return QObject::tr("Offline", "Account status");
case AccountState::Online:
return QObject::tr("Ready", "Account status");
case AccountState::Working:
return QObject::tr("Working", "Account status");
case AccountState::Errored:
return QObject::tr("Errored", "Account status");
case AccountState::Expired:
return QObject::tr("Expired", "Account status");
case AccountState::Disabled:
return QObject::tr("Disabled", "Account status");
case AccountState::Gone:
return QObject::tr("Gone", "Account status");
default:
return QObject::tr("Unknown", "Account status");
}
}
QVariant AccountList::data(const QModelIndex& index, int role) const QVariant AccountList::data(const QModelIndex& index, int role) const
{ {
if (!index.isValid()) if (!index.isValid())
@ -273,13 +297,10 @@ QVariant AccountList::data(const QModelIndex& index, int role) const
switch (role) { switch (role) {
case Qt::DisplayRole: case Qt::DisplayRole:
switch (index.column()) { switch (index.column()) {
case ProfileNameColumn: { case ProfileNameColumn:
return account->profileName(); return account->profileName();
}
case NameColumn: case NameColumn:
return account->accountDisplayString(); return account->accountDisplayString();
case TypeColumn: { case TypeColumn: {
switch (account->accountType()) { switch (account->accountType()) {
case AccountType::MSA: { case AccountType::MSA: {
@ -291,39 +312,8 @@ QVariant AccountList::data(const QModelIndex& index, int role) const
} }
return tr("Unknown", "Account type"); return tr("Unknown", "Account type");
} }
case StatusColumn:
case StatusColumn: { return getAccountStatus(account->accountState());
switch (account->accountState()) {
case AccountState::Unchecked: {
return tr("Unchecked", "Account status");
}
case AccountState::Offline: {
return tr("Offline", "Account status");
}
case AccountState::Online: {
return tr("Ready", "Account status");
}
case AccountState::Working: {
return tr("Working", "Account status");
}
case AccountState::Errored: {
return tr("Errored", "Account status");
}
case AccountState::Expired: {
return tr("Expired", "Account status");
}
case AccountState::Disabled: {
return tr("Disabled", "Account status");
}
case AccountState::Gone: {
return tr("Gone", "Account status");
}
default: {
return tr("Unknown", "Account status");
}
}
}
default: default:
return QVariant(); return QVariant();
} }
@ -335,11 +325,9 @@ QVariant AccountList::data(const QModelIndex& index, int role) const
return QVariant::fromValue(account); return QVariant::fromValue(account);
case Qt::CheckStateRole: case Qt::CheckStateRole:
if (index.column() == ProfileNameColumn) { if (index.column() == ProfileNameColumn)
return account == m_defaultAccount ? Qt::Checked : Qt::Unchecked; return account == m_defaultAccount ? Qt::Checked : Qt::Unchecked;
} else {
return QVariant(); return QVariant();
}
default: default:
return QVariant(); return QVariant();
@ -461,19 +449,15 @@ bool AccountList::loadList()
// Make sure the format version matches. // Make sure the format version matches.
auto listVersion = root.value("formatVersion").toVariant().toInt(); auto listVersion = root.value("formatVersion").toVariant().toInt();
switch (listVersion) { if (listVersion == AccountListVersion::MojangMSA)
case AccountListVersion::MojangMSA: {
return loadV3(root); return loadV3(root);
} break;
default: {
QString newName = "accounts-old.json"; QString newName = "accounts-old.json";
qWarning() << "Unknown format version when loading account list. Existing one will be renamed to" << newName; qWarning() << "Unknown format version when loading account list. Existing one will be renamed to" << newName;
// Attempt to rename the old version. // Attempt to rename the old version.
file.rename(newName); file.rename(newName);
return false; return false;
} }
}
}
bool AccountList::loadV3(QJsonObject& root) bool AccountList::loadV3(QJsonObject& root)
{ {

View file

@ -1,5 +1,4 @@
#include <QDebug> #include <QDebug>
#include <QNetworkAccessManager>
#include <QNetworkReply> #include <QNetworkReply>
#include <QNetworkRequest> #include <QNetworkRequest>

View file

@ -1,12 +1,9 @@
#pragma once #pragma once
#include <QMultiMap>
#include <QString> #include <QString>
#include <memory> #include <memory>
#include "QObjectPtr.h"
class MinecraftAccount; class MinecraftAccount;
class QNetworkAccessManager;
struct AuthSession { struct AuthSession {
bool MakeOffline(QString offline_playername); bool MakeOffline(QString offline_playername);

View file

@ -87,17 +87,13 @@ QVariant GameOptions::data(const QModelIndex& index, int role) const
if (row < 0 || row >= int(contents.size())) if (row < 0 || row >= int(contents.size()))
return QVariant(); return QVariant();
switch (role) { if (role == Qt::DisplayRole) {
case Qt::DisplayRole: if (column == 0)
if (column == 0) {
return contents[row].key; return contents[row].key;
} else {
return contents[row].value; return contents[row].value;
} }
default:
return QVariant(); return QVariant();
} }
}
int GameOptions::rowCount(const QModelIndex&) const int GameOptions::rowCount(const QModelIndex&) const
{ {

View file

@ -105,10 +105,7 @@ std::pair<Version, Version> DataPack::compatibleVersions() const
int DataPack::compare(const Resource& other, SortType type) const int DataPack::compare(const Resource& other, SortType type) const
{ {
auto const& cast_other = static_cast<DataPack const&>(other); auto const& cast_other = static_cast<DataPack const&>(other);
switch (type) { if (type == SortType::PACK_FORMAT) {
default:
return Resource::compare(other, type);
case SortType::PACK_FORMAT: {
auto this_ver = packFormat(); auto this_ver = packFormat();
auto other_ver = cast_other.packFormat(); auto other_ver = cast_other.packFormat();
@ -116,8 +113,8 @@ int DataPack::compare(const Resource& other, SortType type) const
return 1; return 1;
if (this_ver < other_ver) if (this_ver < other_ver)
return -1; return -1;
break; } else {
} return Resource::compare(other, type);
} }
return 0; return 0;
} }

View file

@ -35,8 +35,6 @@ class Version;
class DataPack : public Resource { class DataPack : public Resource {
Q_OBJECT Q_OBJECT
public: public:
using Ptr = shared_qobject_ptr<Resource>;
DataPack(QObject* parent = nullptr) : Resource(parent) {} DataPack(QObject* parent = nullptr) : Resource(parent) {}
DataPack(QFileInfo file_info) : Resource(file_info) {} DataPack(QFileInfo file_info) : Resource(file_info) {}
@ -59,6 +57,8 @@ class DataPack : public Resource {
[[nodiscard]] int compare(Resource const& other, SortType type) const override; [[nodiscard]] int compare(Resource const& other, SortType type) const override;
[[nodiscard]] bool applyFilter(QRegularExpression filter) const override; [[nodiscard]] bool applyFilter(QRegularExpression filter) const override;
virtual QString directory() { return "/data"; }
protected: protected:
mutable QMutex m_data_lock; mutable QMutex m_data_lock;

View file

@ -138,6 +138,15 @@ auto Mod::name() const -> QString
return Resource::name(); return Resource::name();
} }
auto Mod::mod_id() const -> QString
{
auto d_mod_id = details().mod_id;
if (!d_mod_id.isEmpty())
return d_mod_id;
return Resource::name();
}
auto Mod::version() const -> QString auto Mod::version() const -> QString
{ {
return details().version; return details().version;

View file

@ -61,6 +61,7 @@ class Mod : public Resource {
auto details() const -> const ModDetails&; auto details() const -> const ModDetails&;
auto name() const -> QString override; auto name() const -> QString override;
auto mod_id() const -> QString;
auto version() const -> QString; auto version() const -> QString;
auto homepage() const -> QString override; auto homepage() const -> QString override;
auto description() const -> QString; auto description() const -> QString;

View file

@ -145,12 +145,9 @@ QVariant ModFolderModel::data(const QModelIndex& index, int role) const
} }
return {}; return {};
case Qt::CheckStateRole: case Qt::CheckStateRole:
switch (column) { if (column == ActiveColumn)
case ActiveColumn:
return at(row).enabled() ? Qt::Checked : Qt::Unchecked; return at(row).enabled() ? Qt::Checked : Qt::Unchecked;
default:
return QVariant(); return QVariant();
}
default: default:
return QVariant(); return QVariant();
} }

View file

@ -152,6 +152,9 @@ class Resource : public QObject {
[[nodiscard]] bool isMoreThanOneHardLink() const; [[nodiscard]] bool isMoreThanOneHardLink() const;
[[nodiscard]] auto mod_id() const -> QString { return m_mod_id; }
void setModId(const QString& modId) { m_mod_id = modId; }
protected: protected:
/* The file corresponding to this resource. */ /* The file corresponding to this resource. */
QFileInfo m_file_info; QFileInfo m_file_info;
@ -162,6 +165,7 @@ class Resource : public QObject {
QString m_internal_id; QString m_internal_id;
/* Name as reported via the file name. In the absence of a better name, this is shown to the user. */ /* Name as reported via the file name. In the absence of a better name, this is shown to the user. */
QString m_name; QString m_name;
QString m_mod_id;
/* The type of file we're dealing with. */ /* The type of file we're dealing with. */
ResourceType m_type = ResourceType::UNKNOWN; ResourceType m_type = ResourceType::UNKNOWN;

View file

@ -513,12 +513,9 @@ QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const
return {}; return {};
} }
case Qt::CheckStateRole: case Qt::CheckStateRole:
switch (column) { if (column == ActiveColumn)
case ActiveColumn:
return m_resources[row]->enabled() ? Qt::Checked : Qt::Unchecked; return m_resources[row]->enabled() ? Qt::Checked : Qt::Unchecked;
default:
return {}; return {};
}
default: default:
return {}; return {};
} }

View file

@ -42,13 +42,6 @@ void ResourcePack::setPackFormat(int new_format_id)
m_pack_format = new_format_id; m_pack_format = new_format_id;
} }
void ResourcePack::setDescription(QString new_description)
{
QMutexLocker locker(&m_data_lock);
m_description = new_description;
}
void ResourcePack::setImage(QImage new_image) const void ResourcePack::setImage(QImage new_image) const
{ {
QMutexLocker locker(&m_data_lock); QMutexLocker locker(&m_data_lock);
@ -90,7 +83,7 @@ QPixmap ResourcePack::image(QSize size, Qt::AspectRatioMode mode) const
} }
// Imaged got evicted from the cache. Re-process it and retry. // Imaged got evicted from the cache. Re-process it and retry.
ResourcePackUtils::processPackPNG(*this); ResourcePackUtils::processPackPNG(this);
return image(size); return image(size);
} }
@ -102,44 +95,3 @@ std::pair<Version, Version> ResourcePack::compatibleVersions() const
return s_pack_format_versions.constFind(m_pack_format).value(); return s_pack_format_versions.constFind(m_pack_format).value();
} }
int ResourcePack::compare(const Resource& other, SortType type) const
{
auto const& cast_other = static_cast<ResourcePack const&>(other);
switch (type) {
default:
return Resource::compare(other, type);
case SortType::PACK_FORMAT: {
auto this_ver = packFormat();
auto other_ver = cast_other.packFormat();
if (this_ver > other_ver)
return 1;
if (this_ver < other_ver)
return -1;
break;
}
}
return 0;
}
bool ResourcePack::applyFilter(QRegularExpression filter) const
{
if (filter.match(description()).hasMatch())
return true;
if (filter.match(QString::number(packFormat())).hasMatch())
return true;
if (filter.match(compatibleVersions().first.toString()).hasMatch())
return true;
if (filter.match(compatibleVersions().second.toString()).hasMatch())
return true;
return Resource::applyFilter(filter);
}
bool ResourcePack::valid() const
{
return m_pack_format != 0;
}

View file

@ -1,6 +1,7 @@
#pragma once #pragma once
#include "Resource.h" #include "Resource.h"
#include "minecraft/mod/DataPack.h"
#include <QImage> #include <QImage>
#include <QMutex> #include <QMutex>
@ -14,51 +15,27 @@ class Version;
* Store localized descriptions * Store localized descriptions
* */ * */
class ResourcePack : public Resource { class ResourcePack : public DataPack {
Q_OBJECT Q_OBJECT
public: public:
using Ptr = shared_qobject_ptr<Resource>; ResourcePack(QObject* parent = nullptr) : DataPack(parent) {}
ResourcePack(QFileInfo file_info) : DataPack(file_info) {}
ResourcePack(QObject* parent = nullptr) : Resource(parent) {}
ResourcePack(QFileInfo file_info) : Resource(file_info) {}
/** Gets the numerical ID of the pack format. */
[[nodiscard]] int packFormat() const { return m_pack_format; }
/** Gets, respectively, the lower and upper versions supported by the set pack format. */ /** Gets, respectively, the lower and upper versions supported by the set pack format. */
[[nodiscard]] std::pair<Version, Version> compatibleVersions() const; [[nodiscard]] std::pair<Version, Version> compatibleVersions() const;
/** Gets the description of the resource pack. */
[[nodiscard]] QString description() const { return m_description; }
/** Gets the image of the resource pack, converted to a QPixmap for drawing, and scaled to size. */ /** Gets the image of the resource pack, converted to a QPixmap for drawing, and scaled to size. */
[[nodiscard]] QPixmap image(QSize size, Qt::AspectRatioMode mode = Qt::AspectRatioMode::IgnoreAspectRatio) const; [[nodiscard]] QPixmap image(QSize size, Qt::AspectRatioMode mode = Qt::AspectRatioMode::IgnoreAspectRatio) const;
/** Thread-safe. */
void setPackFormat(int new_format_id);
/** Thread-safe. */
void setDescription(QString new_description);
/** Thread-safe. */ /** Thread-safe. */
void setImage(QImage new_image) const; void setImage(QImage new_image) const;
bool valid() const override; /** Thread-safe. */
void setPackFormat(int new_format_id);
[[nodiscard]] int compare(Resource const& other, SortType type) const override; virtual QString directory() { return "/assets"; }
[[nodiscard]] bool applyFilter(QRegularExpression filter) const override;
protected: protected:
mutable QMutex m_data_lock;
/* The 'version' of a resource pack, as defined in the pack.mcmeta file.
* See https://minecraft.wiki/w/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta
*/
int m_pack_format = 0;
/** The resource pack's description, as defined in the pack.mcmeta file.
*/
QString m_description;
/** The resource pack's image file cache key, for access in the QPixmapCache global instance. /** The resource pack's image file cache key, for access in the QPixmapCache global instance.
* *
* The 'was_ever_used' state simply identifies whether the key was never inserted on the cache (true), * The 'was_ever_used' state simply identifies whether the key was never inserted on the cache (true),

View file

@ -44,7 +44,7 @@
#include "Application.h" #include "Application.h"
#include "Version.h" #include "Version.h"
#include "minecraft/mod/tasks/LocalResourcePackParseTask.h" #include "minecraft/mod/tasks/LocalDataPackParseTask.h"
ResourcePackFolderModel::ResourcePackFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent) ResourcePackFolderModel::ResourcePackFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent)
: ResourceFolderModel(dir, instance, is_indexed, create_dir, parent) : ResourceFolderModel(dir, instance, is_indexed, create_dir, parent)
@ -128,12 +128,9 @@ QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const
} }
return {}; return {};
case Qt::CheckStateRole: case Qt::CheckStateRole:
switch (column) { if (column == ActiveColumn)
case ActiveColumn:
return at(row).enabled() ? Qt::Checked : Qt::Unchecked; return at(row).enabled() ? Qt::Checked : Qt::Unchecked;
default:
return {}; return {};
}
default: default:
return {}; return {};
} }
@ -191,5 +188,5 @@ int ResourcePackFolderModel::columnCount(const QModelIndex& parent) const
Task* ResourcePackFolderModel::createParseTask(Resource& resource) Task* ResourcePackFolderModel::createParseTask(Resource& resource)
{ {
return new LocalResourcePackParseTask(m_next_resolution_ticket, static_cast<ResourcePack&>(resource)); return new LocalDataPackParseTask(m_next_resolution_ticket, dynamic_cast<ResourcePack*>(&resource));
} }

View file

@ -23,6 +23,8 @@
#include "FileSystem.h" #include "FileSystem.h"
#include "Json.h" #include "Json.h"
#include "minecraft/mod/ResourcePack.h"
#include "minecraft/mod/tasks/LocalResourcePackParseTask.h"
#include <quazip/quazip.h> #include <quazip/quazip.h>
#include <quazip/quazipdir.h> #include <quazip/quazipdir.h>
@ -32,9 +34,9 @@
namespace DataPackUtils { namespace DataPackUtils {
bool process(DataPack& pack, ProcessingLevel level) bool process(DataPack* pack, ProcessingLevel level)
{ {
switch (pack.type()) { switch (pack->type()) {
case ResourceType::FOLDER: case ResourceType::FOLDER:
return DataPackUtils::processFolder(pack, level); return DataPackUtils::processFolder(pack, level);
case ResourceType::ZIPFILE: case ResourceType::ZIPFILE:
@ -45,16 +47,16 @@ bool process(DataPack& pack, ProcessingLevel level)
} }
} }
bool processFolder(DataPack& pack, ProcessingLevel level) bool processFolder(DataPack* pack, ProcessingLevel level)
{ {
Q_ASSERT(pack.type() == ResourceType::FOLDER); Q_ASSERT(pack->type() == ResourceType::FOLDER);
auto mcmeta_invalid = [&pack]() { auto mcmeta_invalid = [&pack]() {
qWarning() << "Data pack at" << pack.fileinfo().filePath() << "does not have a valid pack.mcmeta"; qWarning() << "Data pack at" << pack->fileinfo().filePath() << "does not have a valid pack.mcmeta";
return false; // the mcmeta is not optional return false; // the mcmeta is not optional
}; };
QFileInfo mcmeta_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.mcmeta")); QFileInfo mcmeta_file_info(FS::PathCombine(pack->fileinfo().filePath(), "pack.mcmeta"));
if (mcmeta_file_info.exists() && mcmeta_file_info.isFile()) { if (mcmeta_file_info.exists() && mcmeta_file_info.isFile()) {
QFile mcmeta_file(mcmeta_file_info.filePath()); QFile mcmeta_file(mcmeta_file_info.filePath());
if (!mcmeta_file.open(QIODevice::ReadOnly)) if (!mcmeta_file.open(QIODevice::ReadOnly))
@ -72,7 +74,7 @@ bool processFolder(DataPack& pack, ProcessingLevel level)
return mcmeta_invalid(); // mcmeta file isn't a valid file return mcmeta_invalid(); // mcmeta file isn't a valid file
} }
QFileInfo data_dir_info(FS::PathCombine(pack.fileinfo().filePath(), "data")); QFileInfo data_dir_info(FS::PathCombine(pack->fileinfo().filePath(), pack->directory()));
if (!data_dir_info.exists() || !data_dir_info.isDir()) { if (!data_dir_info.exists() || !data_dir_info.isDir()) {
return false; // data dir does not exists or isn't valid return false; // data dir does not exists or isn't valid
} }
@ -80,22 +82,46 @@ bool processFolder(DataPack& pack, ProcessingLevel level)
if (level == ProcessingLevel::BasicInfoOnly) { if (level == ProcessingLevel::BasicInfoOnly) {
return true; // only need basic info already checked return true; // only need basic info already checked
} }
if (auto rp = dynamic_cast<ResourcePack*>(pack)) {
auto png_invalid = [&pack]() {
qWarning() << "Resource pack at" << pack->fileinfo().filePath() << "does not have a valid pack.png";
return true; // the png is optional
};
QFileInfo image_file_info(FS::PathCombine(pack->fileinfo().filePath(), "pack.png"));
if (image_file_info.exists() && image_file_info.isFile()) {
QFile pack_png_file(image_file_info.filePath());
if (!pack_png_file.open(QIODevice::ReadOnly))
return png_invalid(); // can't open pack.png file
auto data = pack_png_file.readAll();
bool pack_png_result = ResourcePackUtils::processPackPNG(rp, std::move(data));
pack_png_file.close();
if (!pack_png_result) {
return png_invalid(); // pack.png invalid
}
} else {
return png_invalid(); // pack.png does not exists or is not a valid file.
}
}
return true; // all tests passed return true; // all tests passed
} }
bool processZIP(DataPack& pack, ProcessingLevel level) bool processZIP(DataPack* pack, ProcessingLevel level)
{ {
Q_ASSERT(pack.type() == ResourceType::ZIPFILE); Q_ASSERT(pack->type() == ResourceType::ZIPFILE);
QuaZip zip(pack.fileinfo().filePath()); QuaZip zip(pack->fileinfo().filePath());
if (!zip.open(QuaZip::mdUnzip)) if (!zip.open(QuaZip::mdUnzip))
return false; // can't open zip file return false; // can't open zip file
QuaZipFile file(&zip); QuaZipFile file(&zip);
auto mcmeta_invalid = [&pack]() { auto mcmeta_invalid = [&pack]() {
qWarning() << "Data pack at" << pack.fileinfo().filePath() << "does not have a valid pack.mcmeta"; qWarning() << "Data pack at" << pack->fileinfo().filePath() << "does not have a valid pack.mcmeta";
return false; // the mcmeta is not optional return false; // the mcmeta is not optional
}; };
@ -119,7 +145,7 @@ bool processZIP(DataPack& pack, ProcessingLevel level)
} }
QuaZipDir zipDir(&zip); QuaZipDir zipDir(&zip);
if (!zipDir.exists("/data")) { if (!zipDir.exists(pack->directory())) {
return false; // data dir does not exists at zip root return false; // data dir does not exists at zip root
} }
@ -127,21 +153,49 @@ bool processZIP(DataPack& pack, ProcessingLevel level)
zip.close(); zip.close();
return true; // only need basic info already checked return true; // only need basic info already checked
} }
if (auto rp = dynamic_cast<ResourcePack*>(pack)) {
auto png_invalid = [&pack]() {
qWarning() << "Resource pack at" << pack->fileinfo().filePath() << "does not have a valid pack.png";
return true; // the png is optional
};
if (zip.setCurrentFile("pack.png")) {
if (!file.open(QIODevice::ReadOnly)) {
qCritical() << "Failed to open file in zip.";
zip.close();
return png_invalid();
}
auto data = file.readAll();
bool pack_png_result = ResourcePackUtils::processPackPNG(rp, std::move(data));
file.close();
zip.close();
if (!pack_png_result) {
return png_invalid(); // pack.png invalid
}
} else {
zip.close();
return png_invalid(); // could not set pack.mcmeta as current file.
}
}
zip.close(); zip.close();
return true; return true;
} }
// https://minecraft.wiki/w/Data_pack#pack.mcmeta // https://minecraft.wiki/w/Data_pack#pack.mcmeta
bool processMCMeta(DataPack& pack, QByteArray&& raw_data) // https://minecraft.wiki/w/Raw_JSON_text_format
// https://minecraft.wiki/w/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta
bool processMCMeta(DataPack* pack, QByteArray&& raw_data)
{ {
try { try {
auto json_doc = QJsonDocument::fromJson(raw_data); auto json_doc = QJsonDocument::fromJson(raw_data);
auto pack_obj = Json::requireObject(json_doc.object(), "pack", {}); auto pack_obj = Json::requireObject(json_doc.object(), "pack", {});
pack.setPackFormat(Json::ensureInteger(pack_obj, "pack_format", 0)); pack->setPackFormat(Json::ensureInteger(pack_obj, "pack_format", 0));
pack.setDescription(Json::ensureString(pack_obj, "description", "")); pack->setDescription(ResourcePackUtils::processComponent(pack_obj.value("description")));
} catch (Json::JsonException& e) { } catch (Json::JsonException& e) {
qWarning() << "JsonException: " << e.what() << e.cause(); qWarning() << "JsonException: " << e.what() << e.cause();
return false; return false;
@ -152,26 +206,19 @@ bool processMCMeta(DataPack& pack, QByteArray&& raw_data)
bool validate(QFileInfo file) bool validate(QFileInfo file)
{ {
DataPack dp{ file }; DataPack dp{ file };
return DataPackUtils::process(dp, ProcessingLevel::BasicInfoOnly) && dp.valid(); return DataPackUtils::process(&dp, ProcessingLevel::BasicInfoOnly) && dp.valid();
} }
} // namespace DataPackUtils } // namespace DataPackUtils
LocalDataPackParseTask::LocalDataPackParseTask(int token, DataPack& dp) : Task(false), m_token(token), m_data_pack(dp) {} LocalDataPackParseTask::LocalDataPackParseTask(int token, DataPack* dp) : Task(false), m_token(token), m_data_pack(dp) {}
bool LocalDataPackParseTask::abort()
{
m_aborted = true;
return true;
}
void LocalDataPackParseTask::executeTask() void LocalDataPackParseTask::executeTask()
{ {
if (!DataPackUtils::process(m_data_pack)) if (!DataPackUtils::process(m_data_pack)) {
emitFailed("process failed");
return; return;
}
if (m_aborted)
emitAborted();
else
emitSucceeded(); emitSucceeded();
} }

View file

@ -32,12 +32,12 @@ namespace DataPackUtils {
enum class ProcessingLevel { Full, BasicInfoOnly }; enum class ProcessingLevel { Full, BasicInfoOnly };
bool process(DataPack& pack, ProcessingLevel level = ProcessingLevel::Full); bool process(DataPack* pack, ProcessingLevel level = ProcessingLevel::Full);
bool processZIP(DataPack& pack, ProcessingLevel level = ProcessingLevel::Full); bool processZIP(DataPack* pack, ProcessingLevel level = ProcessingLevel::Full);
bool processFolder(DataPack& pack, ProcessingLevel level = ProcessingLevel::Full); bool processFolder(DataPack* pack, ProcessingLevel level = ProcessingLevel::Full);
bool processMCMeta(DataPack& pack, QByteArray&& raw_data); bool processMCMeta(DataPack* pack, QByteArray&& raw_data);
/** Checks whether a file is valid as a data pack or not. */ /** Checks whether a file is valid as a data pack or not. */
bool validate(QFileInfo file); bool validate(QFileInfo file);
@ -47,10 +47,7 @@ bool validate(QFileInfo file);
class LocalDataPackParseTask : public Task { class LocalDataPackParseTask : public Task {
Q_OBJECT Q_OBJECT
public: public:
LocalDataPackParseTask(int token, DataPack& dp); LocalDataPackParseTask(int token, DataPack* dp);
[[nodiscard]] bool canAbort() const override { return true; }
bool abort() override;
void executeTask() override; void executeTask() override;
@ -59,7 +56,5 @@ class LocalDataPackParseTask : public Task {
private: private:
int m_token; int m_token;
DataPack& m_data_pack; DataPack* m_data_pack;
bool m_aborted = false;
}; };

View file

@ -20,6 +20,7 @@
#include "FileSystem.h" #include "FileSystem.h"
#include "Json.h" #include "Json.h"
#include "minecraft/mod/tasks/LocalDataPackParseTask.h"
#include <quazip/quazip.h> #include <quazip/quazip.h>
#include <quazip/quazipdir.h> #include <quazip/quazipdir.h>
@ -29,155 +30,6 @@
namespace ResourcePackUtils { namespace ResourcePackUtils {
bool process(ResourcePack& pack, ProcessingLevel level)
{
switch (pack.type()) {
case ResourceType::FOLDER:
return ResourcePackUtils::processFolder(pack, level);
case ResourceType::ZIPFILE:
return ResourcePackUtils::processZIP(pack, level);
default:
qWarning() << "Invalid type for resource pack parse task!";
return false;
}
}
bool processFolder(ResourcePack& pack, ProcessingLevel level)
{
Q_ASSERT(pack.type() == ResourceType::FOLDER);
auto mcmeta_invalid = [&pack]() {
qWarning() << "Resource pack at" << pack.fileinfo().filePath() << "does not have a valid pack.mcmeta";
return false; // the mcmeta is not optional
};
QFileInfo mcmeta_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.mcmeta"));
if (mcmeta_file_info.exists() && mcmeta_file_info.isFile()) {
QFile mcmeta_file(mcmeta_file_info.filePath());
if (!mcmeta_file.open(QIODevice::ReadOnly))
return mcmeta_invalid(); // can't open mcmeta file
auto data = mcmeta_file.readAll();
bool mcmeta_result = ResourcePackUtils::processMCMeta(pack, std::move(data));
mcmeta_file.close();
if (!mcmeta_result) {
return mcmeta_invalid(); // mcmeta invalid
}
} else {
return mcmeta_invalid(); // mcmeta file isn't a valid file
}
QFileInfo assets_dir_info(FS::PathCombine(pack.fileinfo().filePath(), "assets"));
if (!assets_dir_info.exists() || !assets_dir_info.isDir()) {
return false; // assets dir does not exists or isn't valid
}
if (level == ProcessingLevel::BasicInfoOnly) {
return true; // only need basic info already checked
}
auto png_invalid = [&pack]() {
qWarning() << "Resource pack at" << pack.fileinfo().filePath() << "does not have a valid pack.png";
return true; // the png is optional
};
QFileInfo image_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.png"));
if (image_file_info.exists() && image_file_info.isFile()) {
QFile pack_png_file(image_file_info.filePath());
if (!pack_png_file.open(QIODevice::ReadOnly))
return png_invalid(); // can't open pack.png file
auto data = pack_png_file.readAll();
bool pack_png_result = ResourcePackUtils::processPackPNG(pack, std::move(data));
pack_png_file.close();
if (!pack_png_result) {
return png_invalid(); // pack.png invalid
}
} else {
return png_invalid(); // pack.png does not exists or is not a valid file.
}
return true; // all tests passed
}
bool processZIP(ResourcePack& pack, ProcessingLevel level)
{
Q_ASSERT(pack.type() == ResourceType::ZIPFILE);
QuaZip zip(pack.fileinfo().filePath());
if (!zip.open(QuaZip::mdUnzip))
return false; // can't open zip file
QuaZipFile file(&zip);
auto mcmeta_invalid = [&pack]() {
qWarning() << "Resource pack at" << pack.fileinfo().filePath() << "does not have a valid pack.mcmeta";
return false; // the mcmeta is not optional
};
if (zip.setCurrentFile("pack.mcmeta")) {
if (!file.open(QIODevice::ReadOnly)) {
qCritical() << "Failed to open file in zip.";
zip.close();
return mcmeta_invalid();
}
auto data = file.readAll();
bool mcmeta_result = ResourcePackUtils::processMCMeta(pack, std::move(data));
file.close();
if (!mcmeta_result) {
return mcmeta_invalid(); // mcmeta invalid
}
} else {
return mcmeta_invalid(); // could not set pack.mcmeta as current file.
}
QuaZipDir zipDir(&zip);
if (!zipDir.exists("/assets")) {
return false; // assets dir does not exists at zip root
}
if (level == ProcessingLevel::BasicInfoOnly) {
zip.close();
return true; // only need basic info already checked
}
auto png_invalid = [&pack]() {
qWarning() << "Resource pack at" << pack.fileinfo().filePath() << "does not have a valid pack.png";
return true; // the png is optional
};
if (zip.setCurrentFile("pack.png")) {
if (!file.open(QIODevice::ReadOnly)) {
qCritical() << "Failed to open file in zip.";
zip.close();
return png_invalid();
}
auto data = file.readAll();
bool pack_png_result = ResourcePackUtils::processPackPNG(pack, std::move(data));
file.close();
zip.close();
if (!pack_png_result) {
return png_invalid(); // pack.png invalid
}
} else {
zip.close();
return png_invalid(); // could not set pack.mcmeta as current file.
}
zip.close();
return true;
}
QString buildStyle(const QJsonObject& obj) QString buildStyle(const QJsonObject& obj)
{ {
QStringList styles; QStringList styles;
@ -259,30 +111,11 @@ QString processComponent(const QJsonValue& value, bool strikethrough, bool under
return {}; return {};
} }
// https://minecraft.wiki/w/Raw_JSON_text_format bool processPackPNG(const ResourcePack* pack, QByteArray&& raw_data)
// https://minecraft.wiki/w/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta
bool processMCMeta(ResourcePack& pack, QByteArray&& raw_data)
{
try {
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.setDescription(processComponent(pack_obj.value("description")));
} catch (Json::JsonException& e) {
qWarning() << "JsonException: " << e.what() << e.cause();
return false;
}
return true;
}
bool processPackPNG(const ResourcePack& pack, QByteArray&& raw_data)
{ {
auto img = QImage::fromData(raw_data); auto img = QImage::fromData(raw_data);
if (!img.isNull()) { if (!img.isNull()) {
pack.setImage(img); pack->setImage(img);
} else { } else {
qWarning() << "Failed to parse pack.png."; qWarning() << "Failed to parse pack.png.";
return false; return false;
@ -290,16 +123,16 @@ bool processPackPNG(const ResourcePack& pack, QByteArray&& raw_data)
return true; return true;
} }
bool processPackPNG(const ResourcePack& pack) bool processPackPNG(const ResourcePack* pack)
{ {
auto png_invalid = [&pack]() { auto png_invalid = [&pack]() {
qWarning() << "Resource pack at" << pack.fileinfo().filePath() << "does not have a valid pack.png"; qWarning() << "Resource pack at" << pack->fileinfo().filePath() << "does not have a valid pack.png";
return false; return false;
}; };
switch (pack.type()) { switch (pack->type()) {
case ResourceType::FOLDER: { case ResourceType::FOLDER: {
QFileInfo image_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.png")); QFileInfo image_file_info(FS::PathCombine(pack->fileinfo().filePath(), "pack.png"));
if (image_file_info.exists() && image_file_info.isFile()) { if (image_file_info.exists() && image_file_info.isFile()) {
QFile pack_png_file(image_file_info.filePath()); QFile pack_png_file(image_file_info.filePath());
if (!pack_png_file.open(QIODevice::ReadOnly)) if (!pack_png_file.open(QIODevice::ReadOnly))
@ -319,7 +152,7 @@ bool processPackPNG(const ResourcePack& pack)
return false; // not processed correctly; https://github.com/PrismLauncher/PrismLauncher/issues/1740 return false; // not processed correctly; https://github.com/PrismLauncher/PrismLauncher/issues/1740
} }
case ResourceType::ZIPFILE: { case ResourceType::ZIPFILE: {
QuaZip zip(pack.fileinfo().filePath()); QuaZip zip(pack->fileinfo().filePath());
if (!zip.open(QuaZip::mdUnzip)) if (!zip.open(QuaZip::mdUnzip))
return false; // can't open zip file return false; // can't open zip file
@ -353,28 +186,7 @@ bool processPackPNG(const ResourcePack& pack)
bool validate(QFileInfo file) bool validate(QFileInfo file)
{ {
ResourcePack rp{ file }; ResourcePack rp{ file };
return ResourcePackUtils::process(rp, ProcessingLevel::BasicInfoOnly) && rp.valid(); return DataPackUtils::process(&rp, DataPackUtils::ProcessingLevel::BasicInfoOnly) && rp.valid();
} }
} // namespace ResourcePackUtils } // namespace ResourcePackUtils
LocalResourcePackParseTask::LocalResourcePackParseTask(int token, ResourcePack& rp) : Task(false), m_token(token), m_resource_pack(rp) {}
bool LocalResourcePackParseTask::abort()
{
m_aborted = true;
return true;
}
void LocalResourcePackParseTask::executeTask()
{
if (!ResourcePackUtils::process(m_resource_pack)) {
emitFailed("this is not a resource pack");
return;
}
if (m_aborted)
emitAborted();
else
emitSucceeded();
}

View file

@ -23,44 +23,14 @@
#include "minecraft/mod/ResourcePack.h" #include "minecraft/mod/ResourcePack.h"
#include "tasks/Task.h"
namespace ResourcePackUtils { namespace ResourcePackUtils {
enum class ProcessingLevel { Full, BasicInfoOnly };
bool process(ResourcePack& pack, ProcessingLevel level = ProcessingLevel::Full);
bool processZIP(ResourcePack& pack, ProcessingLevel level = ProcessingLevel::Full);
bool processFolder(ResourcePack& pack, ProcessingLevel level = ProcessingLevel::Full);
QString processComponent(const QJsonValue& value, bool strikethrough = false, bool underline = false); QString processComponent(const QJsonValue& value, bool strikethrough = false, bool underline = false);
bool processMCMeta(ResourcePack& pack, QByteArray&& raw_data); bool processPackPNG(const ResourcePack* pack, QByteArray&& raw_data);
bool processPackPNG(const ResourcePack& pack, QByteArray&& raw_data);
/// processes ONLY the pack.png (rest of the pack may be invalid) /// processes ONLY the pack.png (rest of the pack may be invalid)
bool processPackPNG(const ResourcePack& pack); bool processPackPNG(const ResourcePack* pack);
/** Checks whether a file is valid as a resource pack or not. */ /** Checks whether a file is valid as a resource pack or not. */
bool validate(QFileInfo file); bool validate(QFileInfo file);
} // namespace ResourcePackUtils } // namespace ResourcePackUtils
class LocalResourcePackParseTask : public Task {
Q_OBJECT
public:
LocalResourcePackParseTask(int token, ResourcePack& rp);
[[nodiscard]] bool canAbort() const override { return true; }
bool abort() override;
void executeTask() override;
[[nodiscard]] int token() const { return m_token; }
private:
int m_token;
ResourcePack& m_resource_pack;
bool m_aborted = false;
};

View file

@ -180,8 +180,10 @@ bool LocalWorldSaveParseTask::abort()
void LocalWorldSaveParseTask::executeTask() void LocalWorldSaveParseTask::executeTask()
{ {
if (!WorldSaveUtils::process(m_save)) if (!WorldSaveUtils::process(m_save)) {
emitFailed("this is not a world");
return; return;
}
if (m_aborted) if (m_aborted)
emitAborted(); emitAborted();

View file

@ -62,7 +62,7 @@ class UserInteractionSupport {
/** /**
* Requests a user interaction to select which optional mods should be installed. * Requests a user interaction to select which optional mods should be installed.
*/ */
virtual std::optional<QVector<QString>> chooseOptionalMods(PackVersion version, QVector<ATLauncher::VersionMod> mods) = 0; virtual std::optional<QVector<QString>> chooseOptionalMods(const PackVersion& version, QVector<ATLauncher::VersionMod> mods) = 0;
/** /**
* Requests a user interaction to select a component version from a given version list * Requests a user interaction to select a component version from a given version list

View file

@ -20,7 +20,6 @@
#include <algorithm> #include <algorithm>
#include "Json.h" #include "Json.h"
#include "QObjectPtr.h"
#include "modplatform/ModIndex.h" #include "modplatform/ModIndex.h"
#include "modplatform/flame/FlameAPI.h" #include "modplatform/flame/FlameAPI.h"
#include "modplatform/flame/FlameModIndex.h" #include "modplatform/flame/FlameModIndex.h"
@ -33,9 +32,7 @@
static const FlameAPI flameAPI; static const FlameAPI flameAPI;
static ModrinthAPI modrinthAPI; static ModrinthAPI modrinthAPI;
Flame::FileResolvingTask::FileResolvingTask(const shared_qobject_ptr<QNetworkAccessManager>& network, Flame::Manifest& toProcess) Flame::FileResolvingTask::FileResolvingTask(Flame::Manifest& toProcess) : m_manifest(toProcess) {}
: m_network(network), m_manifest(toProcess)
{}
bool Flame::FileResolvingTask::abort() bool Flame::FileResolvingTask::abort()
{ {

View file

@ -17,8 +17,6 @@
*/ */
#pragma once #pragma once
#include <QNetworkAccessManager>
#include "PackManifest.h" #include "PackManifest.h"
#include "tasks/Task.h" #include "tasks/Task.h"
@ -26,7 +24,7 @@ namespace Flame {
class FileResolvingTask : public Task { class FileResolvingTask : public Task {
Q_OBJECT Q_OBJECT
public: public:
explicit FileResolvingTask(const shared_qobject_ptr<QNetworkAccessManager>& network, Flame::Manifest& toProcess); explicit FileResolvingTask(Flame::Manifest& toProcess);
virtual ~FileResolvingTask() = default; virtual ~FileResolvingTask() = default;
bool canAbort() const override { return true; } bool canAbort() const override { return true; }
@ -44,7 +42,6 @@ class FileResolvingTask : public Task {
void getFlameProjects(); void getFlameProjects();
private: /* data */ private: /* data */
shared_qobject_ptr<QNetworkAccessManager> m_network;
Flame::Manifest m_manifest; Flame::Manifest m_manifest;
std::shared_ptr<QByteArray> m_result; std::shared_ptr<QByteArray> m_result;
Task::Ptr m_task; Task::Ptr m_task;

View file

@ -102,57 +102,6 @@ QString FlameAPI::getModDescription(int modId)
return description; return description;
} }
QList<ModPlatform::IndexedVersion> FlameAPI::getLatestVersions(VersionSearchArgs&& args)
{
auto versions_url_optional = getVersionsURL(args);
if (!versions_url_optional.has_value())
return {};
auto versions_url = versions_url_optional.value();
QEventLoop loop;
auto netJob = makeShared<NetJob>(QString("Flame::GetLatestVersion(%1)").arg(args.pack.name), APPLICATION->network());
auto response = std::make_shared<QByteArray>();
QList<ModPlatform::IndexedVersion> ver;
netJob->addNetAction(Net::ApiDownload::makeByteArray(versions_url, response));
QObject::connect(netJob.get(), &NetJob::succeeded, [response, args, &ver] {
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from latest mod version at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *response;
return;
}
try {
auto obj = Json::requireObject(doc);
auto arr = Json::requireArray(obj, "data");
for (auto file : arr) {
auto file_obj = Json::requireObject(file);
ver.append(FlameMod::loadIndexedPackVersion(file_obj));
}
} catch (Json::JsonException& e) {
qCritical() << "Failed to parse response from a version request.";
qCritical() << e.what();
qDebug() << doc;
}
});
QObject::connect(netJob.get(), &NetJob::finished, &loop, &QEventLoop::quit);
netJob->start();
loop.exec();
return ver;
}
Task::Ptr FlameAPI::getProjects(QStringList addonIds, std::shared_ptr<QByteArray> response) const Task::Ptr FlameAPI::getProjects(QStringList addonIds, std::shared_ptr<QByteArray> response) const
{ {
auto netJob = makeShared<NetJob>(QString("Flame::GetProjects"), APPLICATION->network()); auto netJob = makeShared<NetJob>(QString("Flame::GetProjects"), APPLICATION->network());
@ -266,7 +215,7 @@ QList<ModPlatform::Category> FlameAPI::loadModCategories(std::shared_ptr<QByteAr
return categories; return categories;
}; };
std::optional<ModPlatform::IndexedVersion> FlameAPI::getLatestVersion(QList<ModPlatform::IndexedVersion> versions, std::optional<ModPlatform::IndexedVersion> FlameAPI::getLatestVersion(QVector<ModPlatform::IndexedVersion> versions,
QList<ModPlatform::ModLoaderType> instanceLoaders, QList<ModPlatform::ModLoaderType> instanceLoaders,
ModPlatform::ModLoaderTypes modLoaders) ModPlatform::ModLoaderTypes modLoaders)
{ {

View file

@ -15,8 +15,7 @@ class FlameAPI : public NetworkResourceAPI {
QString getModFileChangelog(int modId, int fileId); QString getModFileChangelog(int modId, int fileId);
QString getModDescription(int modId); QString getModDescription(int modId);
QList<ModPlatform::IndexedVersion> getLatestVersions(VersionSearchArgs&& args); std::optional<ModPlatform::IndexedVersion> getLatestVersion(QVector<ModPlatform::IndexedVersion> versions,
std::optional<ModPlatform::IndexedVersion> getLatestVersion(QList<ModPlatform::IndexedVersion> versions,
QList<ModPlatform::ModLoaderType> instanceLoaders, QList<ModPlatform::ModLoaderType> instanceLoaders,
ModPlatform::ModLoaderTypes fallback); ModPlatform::ModLoaderTypes fallback);
@ -108,12 +107,6 @@ class FlameAPI : public NetworkResourceAPI {
return "https://api.curseforge.com/v1/mods/search?gameId=432&" + get_arguments.join('&'); return "https://api.curseforge.com/v1/mods/search?gameId=432&" + get_arguments.join('&');
} }
private:
[[nodiscard]] std::optional<QString> getInfoURL(QString const& id) const override
{
return QString("https://api.curseforge.com/v1/mods/%1").arg(id);
}
[[nodiscard]] std::optional<QString> getVersionsURL(VersionSearchArgs const& args) const override [[nodiscard]] std::optional<QString> getVersionsURL(VersionSearchArgs const& args) const override
{ {
auto addonId = args.pack.addonId.toString(); auto addonId = args.pack.addonId.toString();
@ -129,6 +122,11 @@ class FlameAPI : public NetworkResourceAPI {
return url; return url;
} }
private:
[[nodiscard]] std::optional<QString> getInfoURL(QString const& id) const override
{
return QString("https://api.curseforge.com/v1/mods/%1").arg(id);
}
[[nodiscard]] std::optional<QString> getDependencyURL(DependencySearchArgs const& args) const override [[nodiscard]] std::optional<QString> getDependencyURL(DependencySearchArgs const& args) const override
{ {
auto addonId = args.dependency.addonId.toString(); auto addonId = args.dependency.addonId.toString();

View file

@ -3,115 +3,31 @@
#include "FlameAPI.h" #include "FlameAPI.h"
#include "FlameModIndex.h" #include "FlameModIndex.h"
#include <MurmurHash2.h> #include <QHash>
#include <memory> #include <memory>
#include "Json.h" #include "Json.h"
#include "QObjectPtr.h"
#include "ResourceDownloadTask.h" #include "ResourceDownloadTask.h"
#include "minecraft/mod/ModFolderModel.h"
#include "minecraft/mod/tasks/GetModDependenciesTask.h" #include "minecraft/mod/tasks/GetModDependenciesTask.h"
#include "modplatform/ModIndex.h"
#include "net/ApiDownload.h" #include "net/ApiDownload.h"
#include "net/NetJob.h"
#include "tasks/Task.h"
static FlameAPI api; static FlameAPI api;
bool FlameCheckUpdate::abort() bool FlameCheckUpdate::abort()
{ {
m_was_aborted = true; bool result = false;
if (m_net_job) if (m_task && m_task->canAbort()) {
return m_net_job->abort(); result = m_task->abort();
return true;
} }
Task::abort();
ModPlatform::IndexedPack FlameCheckUpdate::getProjectInfo(ModPlatform::IndexedVersion& ver_info) return result;
{
ModPlatform::IndexedPack pack;
QEventLoop loop;
auto get_project_job = new NetJob("Flame::GetProjectJob", APPLICATION->network());
auto response = std::make_shared<QByteArray>();
auto url = QString("https://api.curseforge.com/v1/mods/%1").arg(ver_info.addonId.toString());
auto dl = Net::ApiDownload::makeByteArray(url, response);
get_project_job->addNetAction(dl);
QObject::connect(get_project_job, &NetJob::succeeded, [response, &pack]() {
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from FlameCheckUpdate at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *response;
return;
}
try {
auto doc_obj = Json::requireObject(doc);
auto data_obj = Json::requireObject(doc_obj, "data");
FlameMod::loadIndexedPack(pack, data_obj);
} catch (Json::JsonException& e) {
qWarning() << e.cause();
qDebug() << doc;
}
});
connect(get_project_job, &NetJob::failed, this, &FlameCheckUpdate::emitFailed);
QObject::connect(get_project_job, &NetJob::finished, [&loop, get_project_job] {
get_project_job->deleteLater();
loop.quit();
});
get_project_job->start();
loop.exec();
return pack;
}
ModPlatform::IndexedVersion FlameCheckUpdate::getFileInfo(int addonId, int fileId)
{
ModPlatform::IndexedVersion ver;
QEventLoop loop;
auto get_file_info_job = new NetJob("Flame::GetFileInfoJob", APPLICATION->network());
auto response = std::make_shared<QByteArray>();
auto url = QString("https://api.curseforge.com/v1/mods/%1/files/%2").arg(QString::number(addonId), QString::number(fileId));
auto dl = Net::ApiDownload::makeByteArray(url, response);
get_file_info_job->addNetAction(dl);
QObject::connect(get_file_info_job, &NetJob::succeeded, [response, &ver]() {
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from FlameCheckUpdate at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *response;
return;
}
try {
auto doc_obj = Json::requireObject(doc);
auto data_obj = Json::requireObject(doc_obj, "data");
ver = FlameMod::loadIndexedPackVersion(data_obj);
} catch (Json::JsonException& e) {
qWarning() << e.cause();
qDebug() << doc;
}
});
connect(get_file_info_job, &NetJob::failed, this, &FlameCheckUpdate::emitFailed);
QObject::connect(get_file_info_job, &NetJob::finished, [&loop, get_file_info_job] {
get_file_info_job->deleteLater();
loop.quit();
});
get_file_info_job->start();
loop.exec();
return ver;
} }
/* Check for update: /* Check for update:
@ -123,19 +39,55 @@ void FlameCheckUpdate::executeTask()
{ {
setStatus(tr("Preparing resources for CurseForge...")); setStatus(tr("Preparing resources for CurseForge..."));
int i = 0; auto netJob = new NetJob("Get latest versions", APPLICATION->network());
connect(netJob, &Task::finished, this, &FlameCheckUpdate::collectBlockedMods);
connect(netJob, &Task::progress, this, &FlameCheckUpdate::setProgress);
connect(netJob, &Task::stepProgress, this, &FlameCheckUpdate::propagateStepProgress);
connect(netJob, &Task::details, this, &FlameCheckUpdate::setDetails);
for (auto* resource : m_resources) { for (auto* resource : m_resources) {
setStatus(tr("Getting API response from CurseForge for '%1'...").arg(resource->name())); auto versions_url_optional = api.getVersionsURL({ { resource->metadata()->project_id.toString() }, m_game_versions });
setProgress(i++, m_resources.size()); if (!versions_url_optional.has_value())
continue;
auto latest_vers = api.getLatestVersions({ { resource->metadata()->project_id.toString() }, m_game_versions }); auto response = std::make_shared<QByteArray>();
auto task = Net::ApiDownload::makeByteArray(versions_url_optional.value(), response);
// Check if we were aborted while getting the latest version connect(task.get(), &Task::succeeded, this, [this, resource, response] { getLatestVersionCallback(resource, response); });
if (m_was_aborted) { netJob->addNetAction(task);
aborted(); }
m_task.reset(netJob);
m_task->start();
}
void FlameCheckUpdate::getLatestVersionCallback(Resource* resource, std::shared_ptr<QByteArray> response)
{
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from latest mod version at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *response;
return; return;
} }
auto latest_ver = api.getLatestVersion(latest_vers, m_loaders_list, resource->metadata()->loaders);
// Fake pack with the necessary info to pass to the download task :)
auto pack = std::make_shared<ModPlatform::IndexedPack>();
pack->name = resource->name();
pack->slug = resource->metadata()->slug;
pack->addonId = resource->metadata()->project_id;
pack->provider = ModPlatform::ResourceProvider::FLAME;
try {
auto obj = Json::requireObject(doc);
auto arr = Json::requireArray(obj, "data");
FlameMod::loadIndexedPackVersions(*pack.get(), arr);
} catch (Json::JsonException& e) {
qCritical() << "Failed to parse response from a version request.";
qCritical() << e.what();
qDebug() << doc;
}
auto latest_ver = api.getLatestVersion(pack->versions, m_loaders_list, resource->metadata()->loaders);
setStatus(tr("Parsing the API response from CurseForge for '%1'...").arg(resource->name())); setStatus(tr("Parsing the API response from CurseForge for '%1'...").arg(resource->name()));
@ -149,23 +101,14 @@ void FlameCheckUpdate::executeTask()
reason = tr("No valid version found for this resource. It's probably unavailable for the current game version."); reason = tr("No valid version found for this resource. It's probably unavailable for the current game version.");
emit checkFailed(resource, reason); emit checkFailed(resource, reason);
continue; return;
} }
if (latest_ver->downloadUrl.isEmpty() && latest_ver->fileId != resource->metadata()->file_id) { if (latest_ver->downloadUrl.isEmpty() && latest_ver->fileId != resource->metadata()->file_id) {
auto pack = getProjectInfo(latest_ver.value()); m_blocked[resource] = latest_ver->fileId.toString();
auto recover_url = QString("%1/download/%2").arg(pack.websiteUrl, latest_ver->fileId.toString()); return;
emit checkFailed(resource, tr("Resource has a new update available, but is not downloadable using CurseForge."), recover_url);
continue;
} }
// Fake pack with the necessary info to pass to the download task :)
auto pack = std::make_shared<ModPlatform::IndexedPack>();
pack->name = resource->name();
pack->slug = resource->metadata()->slug;
pack->addonId = resource->metadata()->project_id;
pack->provider = ModPlatform::ResourceProvider::FLAME;
if (!latest_ver->hash.isEmpty() && if (!latest_ver->hash.isEmpty() &&
(resource->metadata()->hash != latest_ver->hash || resource->status() == ResourceStatus::NOT_INSTALLED)) { (resource->metadata()->hash != latest_ver->hash || resource->status() == ResourceStatus::NOT_INSTALLED)) {
auto old_version = resource->metadata()->version_number; auto old_version = resource->metadata()->version_number;
@ -184,5 +127,75 @@ void FlameCheckUpdate::executeTask()
m_deps.append(std::make_shared<GetModDependenciesTask::PackDependency>(pack, latest_ver.value())); m_deps.append(std::make_shared<GetModDependenciesTask::PackDependency>(pack, latest_ver.value()));
} }
emitSucceeded(); void FlameCheckUpdate::collectBlockedMods()
{
QStringList addonIds;
QHash<QString, Resource*> quickSearch;
for (auto const& resource : m_blocked.keys()) {
auto addonId = resource->metadata()->project_id.toString();
addonIds.append(addonId);
quickSearch[addonId] = resource;
}
auto response = std::make_shared<QByteArray>();
Task::Ptr projTask;
if (addonIds.isEmpty()) {
emitSucceeded();
return;
} else if (addonIds.size() == 1) {
projTask = api.getProject(*addonIds.begin(), response);
} else {
projTask = api.getProjects(addonIds, response);
}
connect(projTask.get(), &Task::succeeded, this, [this, response, addonIds, quickSearch] {
QJsonParseError parse_error{};
auto doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from Flame projects task at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *response;
return;
}
try {
QJsonArray entries;
if (addonIds.size() == 1)
entries = { Json::requireObject(Json::requireObject(doc), "data") };
else
entries = Json::requireArray(Json::requireObject(doc), "data");
for (auto entry : entries) {
auto entry_obj = Json::requireObject(entry);
auto id = QString::number(Json::requireInteger(entry_obj, "id"));
auto resource = quickSearch.find(id).value();
ModPlatform::IndexedPack pack;
try {
setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(resource->name()));
FlameMod::loadIndexedPack(pack, entry_obj);
auto recover_url = QString("%1/download/%2").arg(pack.websiteUrl, m_blocked[resource]);
emit checkFailed(resource, tr("Resource has a new update available, but is not downloadable using CurseForge."),
recover_url);
} catch (Json::JsonException& e) {
qDebug() << e.cause();
qDebug() << entries;
}
}
} catch (Json::JsonException& e) {
qDebug() << e.cause();
qDebug() << doc;
}
});
connect(projTask.get(), &Task::finished, this, &FlameCheckUpdate::emitSucceeded); // do not care much about error
connect(projTask.get(), &Task::progress, this, &FlameCheckUpdate::setProgress);
connect(projTask.get(), &Task::stepProgress, this, &FlameCheckUpdate::propagateStepProgress);
connect(projTask.get(), &Task::details, this, &FlameCheckUpdate::setDetails);
m_task.reset(projTask);
m_task->start();
} }

View file

@ -1,7 +1,6 @@
#pragma once #pragma once
#include "modplatform/CheckUpdateTask.h" #include "modplatform/CheckUpdateTask.h"
#include "net/NetJob.h"
class FlameCheckUpdate : public CheckUpdateTask { class FlameCheckUpdate : public CheckUpdateTask {
Q_OBJECT Q_OBJECT
@ -19,12 +18,12 @@ class FlameCheckUpdate : public CheckUpdateTask {
protected slots: protected slots:
void executeTask() override; void executeTask() override;
private slots:
void getLatestVersionCallback(Resource* resource, std::shared_ptr<QByteArray> response);
void collectBlockedMods();
private: private:
ModPlatform::IndexedPack getProjectInfo(ModPlatform::IndexedVersion& ver_info); Task::Ptr m_task = nullptr;
ModPlatform::IndexedVersion getFileInfo(int addonId, int fileId);
NetJob* m_net_job = nullptr; QHash<Resource*, QString> m_blocked;
bool m_was_aborted = false;
}; };

View file

@ -442,7 +442,7 @@ bool FlameCreationTask::createInstance()
instance.setName(name()); instance.setName(name());
m_modIdResolver.reset(new Flame::FileResolvingTask(APPLICATION->network(), m_pack)); m_modIdResolver.reset(new Flame::FileResolvingTask(m_pack));
connect(m_modIdResolver.get(), &Flame::FileResolvingTask::succeeded, this, [this, &loop] { idResolverSucceeded(loop); }); connect(m_modIdResolver.get(), &Flame::FileResolvingTask::succeeded, this, [this, &loop] { idResolverSucceeded(loop); });
connect(m_modIdResolver.get(), &Flame::FileResolvingTask::failed, [this, &loop](QString reason) { connect(m_modIdResolver.get(), &Flame::FileResolvingTask::failed, [this, &loop](QString reason) {
m_modIdResolver.reset(); m_modIdResolver.reset();

View file

@ -76,10 +76,7 @@ static QString enumToString(int hash_algorithm)
} }
} }
void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr)
QJsonArray& arr,
[[maybe_unused]] const shared_qobject_ptr<QNetworkAccessManager>& network,
const BaseInstance* inst)
{ {
QVector<ModPlatform::IndexedVersion> unsortedVersions; QVector<ModPlatform::IndexedVersion> unsortedVersions;
for (auto versionIter : arr) { for (auto versionIter : arr) {

View file

@ -6,7 +6,6 @@
#include "modplatform/ModIndex.h" #include "modplatform/ModIndex.h"
#include <QNetworkAccessManager>
#include "BaseInstance.h" #include "BaseInstance.h"
namespace FlameMod { namespace FlameMod {
@ -14,10 +13,7 @@ namespace FlameMod {
void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj); void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj);
void loadURLs(ModPlatform::IndexedPack& m, QJsonObject& obj); void loadURLs(ModPlatform::IndexedPack& m, QJsonObject& obj);
void loadBody(ModPlatform::IndexedPack& m, QJsonObject& obj); void loadBody(ModPlatform::IndexedPack& m, QJsonObject& obj);
void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr);
QJsonArray& arr, ModPlatform::IndexedVersion loadIndexedPackVersion(QJsonObject& obj, bool load_changelog = false);
const shared_qobject_ptr<QNetworkAccessManager>& network, ModPlatform::IndexedVersion loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr, const BaseInstance* inst);
const BaseInstance* inst);
auto loadIndexedPackVersion(QJsonObject& obj, bool load_changelog = false) -> ModPlatform::IndexedVersion;
auto loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr, const BaseInstance* inst) -> ModPlatform::IndexedVersion;
} // namespace FlameMod } // namespace FlameMod

View file

@ -203,6 +203,7 @@ QString exportToModList(QList<Mod*> mods, QString lineTemplate)
for (auto mod : mods) { for (auto mod : mods) {
auto meta = mod->metadata(); auto meta = mod->metadata();
auto modName = mod->name(); auto modName = mod->name();
auto modID = mod->mod_id();
auto url = mod->homepage(); auto url = mod->homepage();
auto ver = mod->version(); auto ver = mod->version();
if (ver.isEmpty() && meta != nullptr) if (ver.isEmpty() && meta != nullptr)
@ -211,6 +212,7 @@ QString exportToModList(QList<Mod*> mods, QString lineTemplate)
auto filename = mod->fileinfo().fileName(); auto filename = mod->fileinfo().fileName();
lines << QString(lineTemplate) lines << QString(lineTemplate)
.replace("{name}", modName) .replace("{name}", modName)
.replace("{mod_id}", modID)
.replace("{url}", url) .replace("{url}", url)
.replace("{version}", ver) .replace("{version}", ver)
.replace("{authors}", authors) .replace("{authors}", authors)

View file

@ -40,7 +40,7 @@ class PackFetchTask : public QObject {
void failed(QString reason); void failed(QString reason);
void aborted(); void aborted();
void privateFileDownloadFinished(Modpack modpack); void privateFileDownloadFinished(const Modpack& modpack);
void privateFileDownloadFailed(QString reason, QString packCode); void privateFileDownloadFailed(QString reason, QString packCode);
}; };

View file

@ -52,7 +52,7 @@
namespace LegacyFTB { namespace LegacyFTB {
PackInstallTask::PackInstallTask(shared_qobject_ptr<QNetworkAccessManager> network, Modpack pack, QString version) PackInstallTask::PackInstallTask(shared_qobject_ptr<QNetworkAccessManager> network, const Modpack& pack, QString version)
{ {
m_pack = pack; m_pack = pack;
m_version = version; m_version = version;

View file

@ -18,7 +18,7 @@ class PackInstallTask : public InstanceTask {
Q_OBJECT Q_OBJECT
public: public:
explicit PackInstallTask(shared_qobject_ptr<QNetworkAccessManager> network, Modpack pack, QString version); explicit PackInstallTask(shared_qobject_ptr<QNetworkAccessManager> network, const Modpack& pack, QString version);
virtual ~PackInstallTask() {} virtual ~PackInstallTask() {}
bool canAbort() const override { return true; } bool canAbort() const override { return true; }

View file

@ -112,7 +112,7 @@ void Modrinth::loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& ob
pack.extraDataLoaded = true; pack.extraDataLoaded = true;
} }
void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr, const BaseInstance* inst) void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr)
{ {
QVector<ModPlatform::IndexedVersion> unsortedVersions; QVector<ModPlatform::IndexedVersion> unsortedVersions;
for (auto versionIter : arr) { for (auto versionIter : arr) {

View file

@ -19,14 +19,13 @@
#include "modplatform/ModIndex.h" #include "modplatform/ModIndex.h"
#include <QNetworkAccessManager>
#include "BaseInstance.h" #include "BaseInstance.h"
namespace Modrinth { namespace Modrinth {
void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj); void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj);
void loadExtraPackData(ModPlatform::IndexedPack& m, QJsonObject& obj); void loadExtraPackData(ModPlatform::IndexedPack& m, QJsonObject& obj);
void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr, const BaseInstance* inst); void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr);
auto loadIndexedPackVersion(QJsonObject& obj, QString hash_type = "sha512", QString filename_prefer = "") -> ModPlatform::IndexedVersion; auto loadIndexedPackVersion(QJsonObject& obj, QString hash_type = "sha512", QString filename_prefer = "") -> ModPlatform::IndexedVersion;
auto loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr, const BaseInstance* inst) -> ModPlatform::IndexedVersion; auto loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr, const BaseInstance* inst) -> ModPlatform::IndexedVersion;

View file

@ -54,7 +54,7 @@ Task::State FileSink::init(QNetworkRequest& request)
return Task::State::Failed; return Task::State::Failed;
} }
wroteAnyData = false; m_wroteAnyData = false;
m_output_file.reset(new PSaveFile(m_filename)); m_output_file.reset(new PSaveFile(m_filename));
if (!m_output_file->open(QIODevice::WriteOnly)) { if (!m_output_file->open(QIODevice::WriteOnly)) {
qCCritical(taskNetLogC) << "Could not open " + m_filename + " for writing"; qCCritical(taskNetLogC) << "Could not open " + m_filename + " for writing";
@ -72,17 +72,19 @@ Task::State FileSink::write(QByteArray& data)
qCCritical(taskNetLogC) << "Failed writing into " + m_filename; qCCritical(taskNetLogC) << "Failed writing into " + m_filename;
m_output_file->cancelWriting(); m_output_file->cancelWriting();
m_output_file.reset(); m_output_file.reset();
wroteAnyData = false; m_wroteAnyData = false;
return Task::State::Failed; return Task::State::Failed;
} }
wroteAnyData = true; m_wroteAnyData = true;
return Task::State::Running; return Task::State::Running;
} }
Task::State FileSink::abort() Task::State FileSink::abort()
{ {
if (m_output_file) {
m_output_file->cancelWriting(); m_output_file->cancelWriting();
}
failAllValidators(); failAllValidators();
return Task::State::Failed; return Task::State::Failed;
} }
@ -100,7 +102,7 @@ Task::State FileSink::finalize(QNetworkReply& reply)
// if we wrote any data to the save file, we try to commit the data to the real file. // if we wrote any data to the save file, we try to commit the data to the real file.
// if it actually got a proper file, we write it even if it was empty // if it actually got a proper file, we write it even if it was empty
if (gotFile || wroteAnyData) { if (gotFile || m_wroteAnyData) {
// ask validators for data consistency // ask validators for data consistency
// we only do this for actual downloads, not 'your data is still the same' cache hits // we only do this for actual downloads, not 'your data is still the same' cache hits
if (!finalizeAllValidators(reply)) if (!finalizeAllValidators(reply))

View file

@ -58,7 +58,7 @@ class FileSink : public Sink {
protected: protected:
QString m_filename; QString m_filename;
bool wroteAnyData = false; bool m_wroteAnyData = false;
std::unique_ptr<PSaveFile> m_output_file; std::unique_ptr<PSaveFile> m_output_file;
}; };
} // namespace Net } // namespace Net

View file

@ -78,7 +78,7 @@ Task::State MetaCacheSink::finalizeCache(QNetworkReply& reply)
{ {
QFileInfo output_file_info(m_filename); QFileInfo output_file_info(m_filename);
if (wroteAnyData) { if (m_wroteAnyData) {
m_entry->setMD5Sum(m_md5Node->hash().toHex().constData()); m_entry->setMD5Sum(m_md5Node->hash().toHex().constData());
} }

View file

@ -80,7 +80,7 @@ void NetRequest::executeTask()
emit finished(); emit finished();
return; return;
case State::Running: case State::Running:
qCDebug(logCat) << getUid().toString() << "Runninng " << m_url.toString(); qCDebug(logCat) << getUid().toString() << "Running " << m_url.toString();
break; break;
case State::Inactive: case State::Inactive:
case State::Failed: case State::Failed:

View file

@ -1,3 +1,5 @@
#pragma once
#include <SeparatorPrefixTree.h> #include <SeparatorPrefixTree.h>
#include <QRegularExpression> #include <QRegularExpression>
#include "IPathMatcher.h" #include "IPathMatcher.h"

View file

@ -1,3 +1,5 @@
#pragma once
#include <QRegularExpression> #include <QRegularExpression>
#include "IPathMatcher.h" #include "IPathMatcher.h"

View file

@ -123,6 +123,7 @@
#include "KonamiCode.h" #include "KonamiCode.h"
#include "InstanceCopyTask.h" #include "InstanceCopyTask.h"
#include "InstanceDirUpdate.h"
#include "Json.h" #include "Json.h"
@ -288,10 +289,27 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi
view->setSelectionMode(QAbstractItemView::SingleSelection); view->setSelectionMode(QAbstractItemView::SingleSelection);
// FIXME: leaks ListViewDelegate // FIXME: leaks ListViewDelegate
view->setItemDelegate(new ListViewDelegate(this)); auto delegate = new ListViewDelegate(this);
view->setItemDelegate(delegate);
view->setFrameShape(QFrame::NoFrame); view->setFrameShape(QFrame::NoFrame);
// do not show ugly blue border on the mac // do not show ugly blue border on the mac
view->setAttribute(Qt::WA_MacShowFocusRect, false); view->setAttribute(Qt::WA_MacShowFocusRect, false);
connect(delegate, &ListViewDelegate::textChanged, this, [this](QString before, QString after) {
if (auto newRoot = askToUpdateInstanceDirName(m_selectedInstance, before, after, this); !newRoot.isEmpty()) {
auto oldID = m_selectedInstance->id();
auto newID = QFileInfo(newRoot).fileName();
QString origGroup(APPLICATION->instances()->getInstanceGroup(oldID));
bool syncGroup = origGroup != GroupId() && oldID != newID;
if (syncGroup)
APPLICATION->instances()->setInstanceGroup(oldID, GroupId());
refreshInstances();
setSelectedInstanceById(newID);
if (syncGroup)
APPLICATION->instances()->setInstanceGroup(newID, origGroup);
}
});
view->installEventFilter(this); view->installEventFilter(this);
view->setContextMenuPolicy(Qt::CustomContextMenu); view->setContextMenuPolicy(Qt::CustomContextMenu);
@ -1377,20 +1395,8 @@ void MainWindow::on_actionDeleteInstance_triggered()
if (response != QMessageBox::Yes) if (response != QMessageBox::Yes)
return; return;
auto linkedInstances = APPLICATION->instances()->getLinkedInstancesById(id); if (!checkLinkedInstances(id, this, tr("Deleting")))
if (!linkedInstances.empty()) {
response = CustomMessageBox::selectable(this, tr("There are linked instances"),
tr("The following instance(s) might reference files in this instance:\n\n"
"%1\n\n"
"Deleting it could break the other instance(s), \n\n"
"Do you wish to proceed?",
nullptr, linkedInstances.count())
.arg(linkedInstances.join("\n")),
QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No)
->exec();
if (response != QMessageBox::Yes)
return; return;
}
if (APPLICATION->instances()->trashInstance(id)) { if (APPLICATION->instances()->trashInstance(id)) {
ui->actionUndoTrashInstance->setEnabled(APPLICATION->instances()->trashedSomething()); ui->actionUndoTrashInstance->setEnabled(APPLICATION->instances()->trashedSomething());

View file

@ -21,7 +21,8 @@ QMessageBox* selectable(QWidget* parent,
const QString& text, const QString& text,
QMessageBox::Icon icon, QMessageBox::Icon icon,
QMessageBox::StandardButtons buttons, QMessageBox::StandardButtons buttons,
QMessageBox::StandardButton defaultButton) QMessageBox::StandardButton defaultButton,
QCheckBox* checkBox)
{ {
QMessageBox* messageBox = new QMessageBox(parent); QMessageBox* messageBox = new QMessageBox(parent);
messageBox->setWindowTitle(title); messageBox->setWindowTitle(title);
@ -31,6 +32,8 @@ QMessageBox* selectable(QWidget* parent,
messageBox->setTextInteractionFlags(Qt::TextSelectableByMouse); messageBox->setTextInteractionFlags(Qt::TextSelectableByMouse);
messageBox->setIcon(icon); messageBox->setIcon(icon);
messageBox->setTextInteractionFlags(Qt::TextBrowserInteraction); messageBox->setTextInteractionFlags(Qt::TextBrowserInteraction);
if (checkBox)
messageBox->setCheckBox(checkBox);
return messageBox; return messageBox;
} }

View file

@ -23,5 +23,6 @@ QMessageBox* selectable(QWidget* parent,
const QString& text, const QString& text,
QMessageBox::Icon icon = QMessageBox::NoIcon, QMessageBox::Icon icon = QMessageBox::NoIcon,
QMessageBox::StandardButtons buttons = QMessageBox::Ok, QMessageBox::StandardButtons buttons = QMessageBox::Ok,
QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); QMessageBox::StandardButton defaultButton = QMessageBox::NoButton,
QCheckBox* checkBox = nullptr);
} }

View file

@ -79,6 +79,9 @@
<verstretch>0</verstretch> <verstretch>0</verstretch>
</sizepolicy> </sizepolicy>
</property> </property>
<property name="toolTip">
<string>This text supports the following placeholders:&#10;{name} - Mod name&#10;{mod_id} - Mod ID&#10;{url} - Mod URL&#10;{version} - Mod version&#10;{authors} - Mod authors</string>
</property>
</widget> </widget>
</item> </item>
</layout> </layout>

View file

@ -282,6 +282,7 @@ auto ResourceUpdateDialog::ensureMetadata() -> bool
bool skip_rest = false; bool skip_rest = false;
ModPlatform::ResourceProvider provider_rest = ModPlatform::ResourceProvider::MODRINTH; ModPlatform::ResourceProvider provider_rest = ModPlatform::ResourceProvider::MODRINTH;
// adds resource to list based on provider
auto addToTmp = [&modrinth_tmp, &flame_tmp](Resource* resource, ModPlatform::ResourceProvider p) { auto addToTmp = [&modrinth_tmp, &flame_tmp](Resource* resource, ModPlatform::ResourceProvider p) {
switch (p) { switch (p) {
case ModPlatform::ResourceProvider::MODRINTH: case ModPlatform::ResourceProvider::MODRINTH:
@ -293,6 +294,7 @@ auto ResourceUpdateDialog::ensureMetadata() -> bool
} }
}; };
// ask the user on what provider to seach for the mod first
for (auto candidate : m_candidates) { for (auto candidate : m_candidates) {
if (candidate->status() != ResourceStatus::NO_METADATA) { if (candidate->status() != ResourceStatus::NO_METADATA) {
onMetadataEnsured(candidate); onMetadataEnsured(candidate);
@ -335,6 +337,7 @@ auto ResourceUpdateDialog::ensureMetadata() -> bool
addToTmp(candidate, response.chosen); addToTmp(candidate, response.chosen);
} }
// prepare task for the modrinth mods
if (!modrinth_tmp.empty()) { if (!modrinth_tmp.empty()) {
auto modrinth_task = makeShared<EnsureMetadataTask>(modrinth_tmp, index_dir, ModPlatform::ResourceProvider::MODRINTH); auto modrinth_task = makeShared<EnsureMetadataTask>(modrinth_tmp, index_dir, ModPlatform::ResourceProvider::MODRINTH);
connect(modrinth_task.get(), &EnsureMetadataTask::metadataReady, [this](Resource* candidate) { onMetadataEnsured(candidate); }); connect(modrinth_task.get(), &EnsureMetadataTask::metadataReady, [this](Resource* candidate) { onMetadataEnsured(candidate); });
@ -350,6 +353,7 @@ auto ResourceUpdateDialog::ensureMetadata() -> bool
seq.addTask(modrinth_task); seq.addTask(modrinth_task);
} }
// prepare task for the flame mods
if (!flame_tmp.empty()) { if (!flame_tmp.empty()) {
auto flame_task = makeShared<EnsureMetadataTask>(flame_tmp, index_dir, ModPlatform::ResourceProvider::FLAME); auto flame_task = makeShared<EnsureMetadataTask>(flame_tmp, index_dir, ModPlatform::ResourceProvider::FLAME);
connect(flame_task.get(), &EnsureMetadataTask::metadataReady, [this](Resource* candidate) { onMetadataEnsured(candidate); }); connect(flame_task.get(), &EnsureMetadataTask::metadataReady, [this](Resource* candidate) { onMetadataEnsured(candidate); });
@ -367,6 +371,7 @@ auto ResourceUpdateDialog::ensureMetadata() -> bool
seq.addTask(m_second_try_metadata); seq.addTask(m_second_try_metadata);
// execute all the tasks
ProgressDialog checking_dialog(m_parent); ProgressDialog checking_dialog(m_parent);
checking_dialog.setSkipButton(true, tr("Abort")); checking_dialog.setSkipButton(true, tr("Abort"));
checking_dialog.setWindowTitle(tr("Generating metadata...")); checking_dialog.setWindowTitle(tr("Generating metadata..."));
@ -477,13 +482,8 @@ void ResourceUpdateDialog::appendResource(CheckUpdateTask::Update const& info, Q
auto changelog_area = new QTextBrowser(); auto changelog_area = new QTextBrowser();
QString text = info.changelog; QString text = info.changelog;
switch (info.provider) { if (info.provider == ModPlatform::ResourceProvider::MODRINTH) {
case ModPlatform::ResourceProvider::MODRINTH: {
text = markdownToHTML(info.changelog.toUtf8()); text = markdownToHTML(info.changelog.toUtf8());
break;
}
default:
break;
} }
changelog_area->setHtml(StringUtils::htmlListPatch(text)); changelog_area->setHtml(StringUtils::htmlListPatch(text));

View file

@ -223,6 +223,8 @@ void SkinManageDialog::on_capeCombo_currentIndexChanged(int index)
auto cape = m_capes.value(id.toString(), {}); auto cape = m_capes.value(id.toString(), {});
if (!cape.isNull()) { if (!cape.isNull()) {
m_ui->capeImage->setPixmap(previewCape(cape).scaled(size() * (1. / 3), Qt::KeepAspectRatio, Qt::FastTransformation)); m_ui->capeImage->setPixmap(previewCape(cape).scaled(size() * (1. / 3), Qt::KeepAspectRatio, Qt::FastTransformation));
} else {
m_ui->capeImage->clear();
} }
m_skinPreview->updateCape(cape); m_skinPreview->updateCape(cape);
if (auto skin = getSelectedSkin(); skin) { if (auto skin = getSelectedSkin(); skin) {
@ -522,6 +524,8 @@ void SkinManageDialog::resizeEvent(QResizeEvent* event)
auto cape = m_capes.value(id.toString(), {}); auto cape = m_capes.value(id.toString(), {});
if (!cape.isNull()) { if (!cape.isNull()) {
m_ui->capeImage->setPixmap(previewCape(cape).scaled(s, Qt::KeepAspectRatio, Qt::FastTransformation)); m_ui->capeImage->setPixmap(previewCape(cape).scaled(s, Qt::KeepAspectRatio, Qt::FastTransformation));
} else {
m_ui->capeImage->clear();
} }
} }

View file

@ -397,6 +397,7 @@ void ListViewDelegate::setModelData(QWidget* editor, QAbstractItemModel* model,
// Prevent instance names longer than 128 chars // Prevent instance names longer than 128 chars
text.truncate(128); text.truncate(128);
if (text.size() != 0) { if (text.size() != 0) {
emit textChanged(model->data(index).toString(), text);
model->setData(index, text); model->setData(index, text);
} }
} }

View file

@ -33,6 +33,9 @@ class ListViewDelegate : public QStyledItemDelegate {
void setEditorData(QWidget* editor, const QModelIndex& index) const override; void setEditorData(QWidget* editor, const QModelIndex& index) const override;
void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const override; void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const override;
signals:
void textChanged(QString before, QString after) const;
private slots: private slots:
void editingDone(); void editingDone();
}; };

View file

@ -66,7 +66,7 @@ class AccountListPage : public QMainWindow, public BasePage {
return icon; return icon;
} }
QString id() const override { return "accounts"; } QString id() const override { return "accounts"; }
QString helpPage() const override { return "/getting-started/adding-an-account"; } QString helpPage() const override { return "getting-started/adding-an-account"; }
void retranslate() override; void retranslate() override;
public slots: public slots:

View file

@ -65,6 +65,15 @@ enum InstSortMode {
Sort_LastLaunch Sort_LastLaunch
}; };
enum InstRenamingMode {
// Rename metadata only.
Rename_Always,
// Ask everytime.
Rename_Ask,
// Rename physical directory too.
Rename_Never
};
LauncherPage::LauncherPage(QWidget* parent) : QWidget(parent), ui(new Ui::LauncherPage) LauncherPage::LauncherPage(QWidget* parent) : QWidget(parent), ui(new Ui::LauncherPage)
{ {
ui->setupUi(this); ui->setupUi(this);
@ -221,6 +230,7 @@ void LauncherPage::applySettings()
s->set("DownloadsDirWatchRecursive", ui->downloadsDirWatchRecursiveCheckBox->isChecked()); s->set("DownloadsDirWatchRecursive", ui->downloadsDirWatchRecursiveCheckBox->isChecked());
s->set("MoveModsFromDownloadsDir", ui->downloadsDirMoveCheckBox->isChecked()); s->set("MoveModsFromDownloadsDir", ui->downloadsDirMoveCheckBox->isChecked());
// Instance
auto sortMode = (InstSortMode)ui->sortingModeGroup->checkedId(); auto sortMode = (InstSortMode)ui->sortingModeGroup->checkedId();
switch (sortMode) { switch (sortMode) {
case Sort_LastLaunch: case Sort_LastLaunch:
@ -232,6 +242,20 @@ void LauncherPage::applySettings()
break; break;
} }
auto renamingMode = (InstRenamingMode)ui->renamingBehaviorComboBox->currentIndex();
switch (renamingMode) {
case Rename_Always:
s->set("InstRenamingMode", "MetadataOnly");
break;
case Rename_Never:
s->set("InstRenamingMode", "PhysicalDir");
break;
case Rename_Ask:
default:
s->set("InstRenamingMode", "AskEverytime");
break;
}
// Mods // Mods
s->set("ModMetadataDisabled", !ui->metadataEnableBtn->isChecked()); s->set("ModMetadataDisabled", !ui->metadataEnableBtn->isChecked());
s->set("ModDependenciesDisabled", !ui->dependenciesEnableBtn->isChecked()); s->set("ModDependenciesDisabled", !ui->dependenciesEnableBtn->isChecked());
@ -267,14 +291,25 @@ void LauncherPage::loadSettings()
ui->downloadsDirWatchRecursiveCheckBox->setChecked(s->get("DownloadsDirWatchRecursive").toBool()); ui->downloadsDirWatchRecursiveCheckBox->setChecked(s->get("DownloadsDirWatchRecursive").toBool());
ui->downloadsDirMoveCheckBox->setChecked(s->get("MoveModsFromDownloadsDir").toBool()); ui->downloadsDirMoveCheckBox->setChecked(s->get("MoveModsFromDownloadsDir").toBool());
// Instance
QString sortMode = s->get("InstSortMode").toString(); QString sortMode = s->get("InstSortMode").toString();
if (sortMode == "LastLaunch") { if (sortMode == "LastLaunch") {
ui->sortLastLaunchedBtn->setChecked(true); ui->sortLastLaunchedBtn->setChecked(true);
} else { } else {
ui->sortByNameBtn->setChecked(true); ui->sortByNameBtn->setChecked(true);
} }
QString renamingMode = s->get("InstRenamingMode").toString();
InstRenamingMode renamingModeEnum;
if (renamingMode == "MetadataOnly") {
renamingModeEnum = Rename_Always;
} else if (renamingMode == "PhysicalDir") {
renamingModeEnum = Rename_Never;
} else {
renamingModeEnum = Rename_Ask;
}
ui->renamingBehaviorComboBox->setCurrentIndex(renamingModeEnum);
// Mods // Mods
ui->metadataEnableBtn->setChecked(!s->get("ModMetadataDisabled").toBool()); ui->metadataEnableBtn->setChecked(!s->get("ModMetadataDisabled").toBool());
ui->metadataWarningLabel->setHidden(ui->metadataEnableBtn->isChecked()); ui->metadataWarningLabel->setHidden(ui->metadataEnableBtn->isChecked());

View file

@ -43,7 +43,7 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>575</width> <width>575</width>
<height>1294</height> <height>1368</height>
</rect> </rect>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_8"> <layout class="QVBoxLayout" name="verticalLayout_8">
@ -99,6 +99,51 @@
</property> </property>
</spacer> </spacer>
</item> </item>
<item>
<widget class="QLabel" name="renamingBehaviorLabel">
<property name="text">
<string>When renaming instances...</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="renamingBehaviorComboBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<item>
<property name="text">
<string>Ask what to do with the folder</string>
</property>
</item>
<item>
<property name="text">
<string>Always rename the folder</string>
</property>
</item>
<item>
<property name="text">
<string>Never rename the folder - only the displayed name</string>
</property>
</item>
</widget>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>6</height>
</size>
</property>
</spacer>
</item>
<item> <item>
<widget class="QCheckBox" name="preferMenuBarCheckBox"> <widget class="QCheckBox" name="preferMenuBarCheckBox">
<property name="toolTip"> <property name="toolTip">
@ -185,29 +230,16 @@
<string>Folders</string> <string>Folders</string>
</property> </property>
<layout class="QGridLayout" name="foldersBoxLayout"> <layout class="QGridLayout" name="foldersBoxLayout">
<item row="3" column="0"> <item row="2" column="0">
<widget class="QLabel" name="labelJavaDir"> <widget class="QLabel" name="labelIconsDir">
<property name="text"> <property name="text">
<string>&amp;Java</string> <string>&amp;Icons</string>
</property> </property>
<property name="buddy"> <property name="buddy">
<cstring>javaDirTextBox</cstring> <cstring>iconsDirTextBox</cstring>
</property> </property>
</widget> </widget>
</item> </item>
<item row="0" column="0">
<widget class="QLabel" name="labelInstDir">
<property name="text">
<string>I&amp;nstances</string>
</property>
<property name="buddy">
<cstring>instDirTextBox</cstring>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="iconsDirTextBox"/>
</item>
<item row="8" column="0"> <item row="8" column="0">
<widget class="QLabel" name="labelDownloadsDir"> <widget class="QLabel" name="labelDownloadsDir">
<property name="text"> <property name="text">
@ -218,56 +250,13 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="0"> <item row="3" column="0">
<widget class="QLabel" name="labelIconsDir"> <widget class="QLabel" name="labelJavaDir">
<property name="text"> <property name="text">
<string>&amp;Icons</string> <string>&amp;Java</string>
</property> </property>
<property name="buddy"> <property name="buddy">
<cstring>iconsDirTextBox</cstring> <cstring>javaDirTextBox</cstring>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLineEdit" name="skinsDirTextBox"/>
</item>
<item row="1" column="2">
<widget class="QPushButton" name="modsDirBrowseBtn">
<property name="text">
<string>Browse</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="javaDirTextBox"/>
</item>
<item row="8" column="1">
<widget class="QLineEdit" name="downloadsDirTextBox"/>
</item>
<item row="8" column="2">
<widget class="QPushButton" name="downloadsDirBrowseBtn">
<property name="text">
<string>Browse</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="modsDirTextBox"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="labelModsDir">
<property name="text">
<string>&amp;Mods</string>
</property>
<property name="buddy">
<cstring>modsDirTextBox</cstring>
</property>
</widget>
</item>
<item row="3" column="2">
<widget class="QPushButton" name="javaDirBrowseBtn">
<property name="text">
<string>Browse</string>
</property> </property>
</widget> </widget>
</item> </item>
@ -281,19 +270,18 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="0" column="2"> <item row="0" column="0">
<widget class="QPushButton" name="instDirBrowseBtn"> <widget class="QLabel" name="labelInstDir">
<property name="text"> <property name="text">
<string>Browse</string> <string>I&amp;nstances</string>
</property>
<property name="buddy">
<cstring>instDirTextBox</cstring>
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="2"> <item row="3" column="1">
<widget class="QPushButton" name="iconsDirBrowseBtn"> <widget class="QLineEdit" name="javaDirTextBox"/>
<property name="text">
<string>Browse</string>
</property>
</widget>
</item> </item>
<item row="4" column="2"> <item row="4" column="2">
<widget class="QPushButton" name="skinsDirBrowseBtn"> <widget class="QPushButton" name="skinsDirBrowseBtn">
@ -302,9 +290,66 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="8" column="1">
<widget class="QLineEdit" name="downloadsDirTextBox"/>
</item>
<item row="2" column="2">
<widget class="QPushButton" name="iconsDirBrowseBtn">
<property name="text">
<string>Browse</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLineEdit" name="skinsDirTextBox"/>
</item>
<item row="0" column="1"> <item row="0" column="1">
<widget class="QLineEdit" name="instDirTextBox"/> <widget class="QLineEdit" name="instDirTextBox"/>
</item> </item>
<item row="2" column="1">
<widget class="QLineEdit" name="iconsDirTextBox"/>
</item>
<item row="0" column="2">
<widget class="QPushButton" name="instDirBrowseBtn">
<property name="text">
<string>Browse</string>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QPushButton" name="modsDirBrowseBtn">
<property name="text">
<string>Browse</string>
</property>
</widget>
</item>
<item row="8" column="2">
<widget class="QPushButton" name="downloadsDirBrowseBtn">
<property name="text">
<string>Browse</string>
</property>
</widget>
</item>
<item row="3" column="2">
<widget class="QPushButton" name="javaDirBrowseBtn">
<property name="text">
<string>Browse</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="labelModsDir">
<property name="text">
<string>&amp;Mods</string>
</property>
<property name="buddy">
<cstring>modsDirTextBox</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="modsDirTextBox"/>
</item>
</layout> </layout>
</widget> </widget>
</item> </item>
@ -620,7 +665,6 @@
<tabstop>downloadsDirBrowseBtn</tabstop> <tabstop>downloadsDirBrowseBtn</tabstop>
<tabstop>metadataEnableBtn</tabstop> <tabstop>metadataEnableBtn</tabstop>
<tabstop>dependenciesEnableBtn</tabstop> <tabstop>dependenciesEnableBtn</tabstop>
<tabstop>sortLastLaunchedBtn</tabstop>
<tabstop>sortByNameBtn</tabstop> <tabstop>sortByNameBtn</tabstop>
</tabstops> </tabstops>
<resources/> <resources/>

View file

@ -245,7 +245,6 @@ ModrinthManagedPackPage::ModrinthManagedPackPage(BaseInstance* inst, InstanceWin
} }
// MODRINTH // MODRINTH
void ModrinthManagedPackPage::parseManagedPack() void ModrinthManagedPackPage::parseManagedPack()
{ {
qDebug() << "Parsing Modrinth pack"; qDebug() << "Parsing Modrinth pack";
@ -338,6 +337,25 @@ void ModrinthManagedPackPage::suggestVersion()
ManagedPackPage::suggestVersion(); ManagedPackPage::suggestVersion();
} }
/// @brief Called when the update task has completed.
/// Internally handles the closing of the instance window if the update was successful and shows a message box.
/// @param did_succeed Whether the update task was successful.
void ManagedPackPage::onUpdateTaskCompleted(bool did_succeed) const
{
// Close the window if the update was successful
if (did_succeed) {
if (m_instance_window != nullptr)
m_instance_window->close();
CustomMessageBox::selectable(nullptr, tr("Update Successful"), tr("The instance updated to pack version %1 successfully.").arg(m_inst->getManagedPackVersionName()), QMessageBox::Information)
->show();
} else {
CustomMessageBox::selectable(nullptr, tr("Update Failed"), tr("The instance failed to update to pack version %1. Please check launcher logs for more information.").arg(m_inst->getManagedPackVersionName()), QMessageBox::Critical)
->show();
}
}
void ModrinthManagedPackPage::update() void ModrinthManagedPackPage::update()
{ {
auto index = ui->versionsComboBox->currentIndex(); auto index = ui->versionsComboBox->currentIndex();
@ -363,10 +381,9 @@ void ModrinthManagedPackPage::update()
extracted->setIcon(m_inst->iconKey()); extracted->setIcon(m_inst->iconKey());
extracted->setConfirmUpdate(false); extracted->setConfirmUpdate(false);
// Run our task then handle the result
auto did_succeed = runUpdateTask(extracted); auto did_succeed = runUpdateTask(extracted);
onUpdateTaskCompleted(did_succeed);
if (m_instance_window && did_succeed)
m_instance_window->close();
} }
void ModrinthManagedPackPage::updateFromFile() void ModrinthManagedPackPage::updateFromFile()
@ -386,14 +403,12 @@ void ModrinthManagedPackPage::updateFromFile()
extracted->setIcon(m_inst->iconKey()); extracted->setIcon(m_inst->iconKey());
extracted->setConfirmUpdate(false); extracted->setConfirmUpdate(false);
// Run our task then handle the result
auto did_succeed = runUpdateTask(extracted); auto did_succeed = runUpdateTask(extracted);
onUpdateTaskCompleted(did_succeed);
if (m_instance_window && did_succeed)
m_instance_window->close();
} }
// FLAME // FLAME
FlameManagedPackPage::FlameManagedPackPage(BaseInstance* inst, InstanceWindow* instance_window, QWidget* parent) FlameManagedPackPage::FlameManagedPackPage(BaseInstance* inst, InstanceWindow* instance_window, QWidget* parent)
: ManagedPackPage(inst, instance_window, parent) : ManagedPackPage(inst, instance_window, parent)
{ {
@ -531,9 +546,7 @@ void FlameManagedPackPage::update()
extracted->setConfirmUpdate(false); extracted->setConfirmUpdate(false);
auto did_succeed = runUpdateTask(extracted); auto did_succeed = runUpdateTask(extracted);
onUpdateTaskCompleted(did_succeed);
if (m_instance_window && did_succeed)
m_instance_window->close();
} }
void FlameManagedPackPage::updateFromFile() void FlameManagedPackPage::updateFromFile()
@ -555,8 +568,6 @@ void FlameManagedPackPage::updateFromFile()
extracted->setConfirmUpdate(false); extracted->setConfirmUpdate(false);
auto did_succeed = runUpdateTask(extracted); auto did_succeed = runUpdateTask(extracted);
onUpdateTaskCompleted(did_succeed);
if (m_instance_window && did_succeed)
m_instance_window->close();
} }
#include "ManagedPackPage.moc" #include "ManagedPackPage.moc"

View file

@ -94,6 +94,8 @@ class ManagedPackPage : public QWidget, public BasePage {
BaseInstance* m_inst; BaseInstance* m_inst;
bool m_loaded = false; bool m_loaded = false;
void onUpdateTaskCompleted(bool did_succeed) const;
}; };
/** Simple page for when we aren't a managed pack. */ /** Simple page for when we aren't a managed pack. */

View file

@ -36,9 +36,9 @@
*/ */
#include "ServersPage.h" #include "ServersPage.h"
#include "ServerPingTask.h"
#include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/CustomMessageBox.h"
#include "ui_ServersPage.h" #include "ui_ServersPage.h"
#include "ServerPingTask.h"
#include <FileSystem.h> #include <FileSystem.h>
#include <io/stream_reader.h> #include <io/stream_reader.h>
@ -49,10 +49,10 @@
#include <tag_string.h> #include <tag_string.h>
#include <sstream> #include <sstream>
#include <tasks/ConcurrentTask.h>
#include <QFileSystemWatcher> #include <QFileSystemWatcher>
#include <QMenu> #include <QMenu>
#include <QTimer> #include <QTimer>
#include <tasks/ConcurrentTask.h>
static const int COLUMN_COUNT = 3; // 3 , TBD: latency and other nice things. static const int COLUMN_COUNT = 3; // 3 , TBD: latency and other nice things.
@ -317,10 +317,10 @@ class ServersModel : public QAbstractListModel {
if (row < 0 || row >= m_servers.size()) if (row < 0 || row >= m_servers.size())
return QVariant(); return QVariant();
switch (column) {
case 0:
switch (role) { switch (role) {
case Qt::DecorationRole: { case Qt::DecorationRole: {
switch (column) {
case 0: {
auto& bytes = m_servers[row].m_icon; auto& bytes = m_servers[row].m_icon;
if (bytes.size()) { if (bytes.size()) {
QPixmap px; QPixmap px;
@ -329,31 +329,32 @@ class ServersModel : public QAbstractListModel {
} }
return APPLICATION->getThemedIcon("unknown_server"); return APPLICATION->getThemedIcon("unknown_server");
} }
case Qt::DisplayRole:
return m_servers[row].m_name;
case ServerPtrRole:
return QVariant::fromValue<void*>((void*)&m_servers[row]);
default:
return QVariant();
}
case 1: case 1:
switch (role) {
case Qt::DisplayRole:
return m_servers[row].m_address; return m_servers[row].m_address;
default: default:
return QVariant(); return QVariant();
} }
case 2: case 2:
switch (role) { if (role == Qt::DisplayRole) {
case Qt::DisplayRole:
if (m_servers[row].m_currentPlayers) { if (m_servers[row].m_currentPlayers) {
return *m_servers[row].m_currentPlayers; return *m_servers[row].m_currentPlayers;
} else { } else {
return "..."; return "...";
} }
default: } else {
return QVariant(); return QVariant();
} }
}
case Qt::DisplayRole:
if (column == 0)
return m_servers[row].m_name;
else
return QVariant();
case ServerPtrRole:
if (column == 0)
return QVariant::fromValue<void*>((void*)&m_servers[row]);
else
return QVariant();
default: default:
return QVariant(); return QVariant();
} }
@ -447,8 +448,7 @@ class ServersModel : public QAbstractListModel {
} }
m_currentQueryTask = ConcurrentTask::Ptr( m_currentQueryTask = ConcurrentTask::Ptr(
new ConcurrentTask("Query servers status", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt()) new ConcurrentTask("Query servers status", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt()));
);
int row = 0; int row = 0;
for (Server& server : m_servers) { for (Server& server : m_servers) {
// reset current players // reset current players
@ -462,7 +462,8 @@ class ServersModel : public QAbstractListModel {
// Update the model when the task is done // Update the model when the task is done
connect(task, &Task::finished, this, [this, task, row]() { connect(task, &Task::finished, this, [this, task, row]() {
if (m_servers.size() < row) return; if (m_servers.size() < row)
return;
m_servers[row].m_currentPlayers = task->m_outputOnlinePlayers; m_servers[row].m_currentPlayers = task->m_outputOnlinePlayers;
emit dataChanged(index(row, 0), index(row, COLUMN_COUNT - 1)); emit dataChanged(index(row, 0), index(row, COLUMN_COUNT - 1));
}); });

View file

@ -166,12 +166,9 @@ void WorldListPage::retranslate()
bool WorldListPage::worldListFilter(QKeyEvent* keyEvent) bool WorldListPage::worldListFilter(QKeyEvent* keyEvent)
{ {
switch (keyEvent->key()) { if (keyEvent->key() == Qt::Key_Delete) {
case Qt::Key_Delete:
on_actionRemove_triggered(); on_actionRemove_triggered();
return true; return true;
default:
break;
} }
return QWidget::eventFilter(ui->worldTreeView, keyEvent); return QWidget::eventFilter(ui->worldTreeView, keyEvent);
} }

View file

@ -45,7 +45,9 @@
#include "net/ApiDownload.h" #include "net/ApiDownload.h"
AtlOptionalModListModel::AtlOptionalModListModel(QWidget* parent, ATLauncher::PackVersion version, QVector<ATLauncher::VersionMod> mods) AtlOptionalModListModel::AtlOptionalModListModel(QWidget* parent,
const ATLauncher::PackVersion& version,
QVector<ATLauncher::VersionMod> mods)
: QAbstractListModel(parent), m_version(version), m_mods(mods) : QAbstractListModel(parent), m_version(version), m_mods(mods)
{ {
// fill mod index // fill mod index
@ -233,7 +235,7 @@ void AtlOptionalModListModel::clearAll()
emit dataChanged(AtlOptionalModListModel::index(0, EnabledColumn), AtlOptionalModListModel::index(m_mods.size() - 1, EnabledColumn)); emit dataChanged(AtlOptionalModListModel::index(0, EnabledColumn), AtlOptionalModListModel::index(m_mods.size() - 1, EnabledColumn));
} }
void AtlOptionalModListModel::toggleMod(ATLauncher::VersionMod mod, int index) void AtlOptionalModListModel::toggleMod(const ATLauncher::VersionMod& mod, int index)
{ {
auto enable = !m_selection[mod.name]; auto enable = !m_selection[mod.name];
@ -251,7 +253,7 @@ void AtlOptionalModListModel::toggleMod(ATLauncher::VersionMod mod, int index)
setMod(mod, index, enable); setMod(mod, index, enable);
} }
void AtlOptionalModListModel::setMod(ATLauncher::VersionMod mod, int index, bool enable, bool shouldEmit) void AtlOptionalModListModel::setMod(const ATLauncher::VersionMod& mod, int index, bool enable, bool shouldEmit)
{ {
if (m_selection[mod.name] == enable) if (m_selection[mod.name] == enable)
return; return;
@ -313,7 +315,7 @@ void AtlOptionalModListModel::setMod(ATLauncher::VersionMod mod, int index, bool
} }
} }
AtlOptionalModDialog::AtlOptionalModDialog(QWidget* parent, ATLauncher::PackVersion version, QVector<ATLauncher::VersionMod> mods) AtlOptionalModDialog::AtlOptionalModDialog(QWidget* parent, const ATLauncher::PackVersion& version, QVector<ATLauncher::VersionMod> mods)
: QDialog(parent), ui(new Ui::AtlOptionalModDialog) : QDialog(parent), ui(new Ui::AtlOptionalModDialog)
{ {
ui->setupUi(this); ui->setupUi(this);

View file

@ -55,7 +55,7 @@ class AtlOptionalModListModel : public QAbstractListModel {
DescriptionColumn, DescriptionColumn,
}; };
AtlOptionalModListModel(QWidget* parent, ATLauncher::PackVersion version, QVector<ATLauncher::VersionMod> mods); AtlOptionalModListModel(QWidget* parent, const ATLauncher::PackVersion& version, QVector<ATLauncher::VersionMod> mods);
QVector<QString> getResult(); QVector<QString> getResult();
@ -78,8 +78,8 @@ class AtlOptionalModListModel : public QAbstractListModel {
void clearAll(); void clearAll();
private: private:
void toggleMod(ATLauncher::VersionMod mod, int index); void toggleMod(const ATLauncher::VersionMod& mod, int index);
void setMod(ATLauncher::VersionMod mod, int index, bool enable, bool shouldEmit = true); void setMod(const ATLauncher::VersionMod& mod, int index, bool enable, bool shouldEmit = true);
private: private:
NetJob::Ptr m_jobPtr; NetJob::Ptr m_jobPtr;
@ -97,7 +97,7 @@ class AtlOptionalModDialog : public QDialog {
Q_OBJECT Q_OBJECT
public: public:
AtlOptionalModDialog(QWidget* parent, ATLauncher::PackVersion version, QVector<ATLauncher::VersionMod> mods); AtlOptionalModDialog(QWidget* parent, const ATLauncher::PackVersion& version, QVector<ATLauncher::VersionMod> mods);
~AtlOptionalModDialog() override; ~AtlOptionalModDialog() override;
QVector<QString> getResult() { return listModel->getResult(); } QVector<QString> getResult() { return listModel->getResult(); }

View file

@ -41,7 +41,7 @@
AtlUserInteractionSupportImpl::AtlUserInteractionSupportImpl(QWidget* parent) : m_parent(parent) {} AtlUserInteractionSupportImpl::AtlUserInteractionSupportImpl(QWidget* parent) : m_parent(parent) {}
std::optional<QVector<QString>> AtlUserInteractionSupportImpl::chooseOptionalMods(ATLauncher::PackVersion version, std::optional<QVector<QString>> AtlUserInteractionSupportImpl::chooseOptionalMods(const ATLauncher::PackVersion& version,
QVector<ATLauncher::VersionMod> mods) QVector<ATLauncher::VersionMod> mods)
{ {
AtlOptionalModDialog optionalModDialog(m_parent, version, mods); AtlOptionalModDialog optionalModDialog(m_parent, version, mods);

View file

@ -48,7 +48,8 @@ class AtlUserInteractionSupportImpl : public QObject, public ATLauncher::UserInt
private: private:
QString chooseVersion(Meta::VersionList::Ptr vlist, QString minecraftVersion) override; QString chooseVersion(Meta::VersionList::Ptr vlist, QString minecraftVersion) override;
std::optional<QVector<QString>> chooseOptionalMods(ATLauncher::PackVersion version, QVector<ATLauncher::VersionMod> mods) override; std::optional<QVector<QString>> chooseOptionalMods(const ATLauncher::PackVersion& version,
QVector<ATLauncher::VersionMod> mods) override;
void displayMessage(QString message) override; void displayMessage(QString message) override;
private: private:

View file

@ -32,7 +32,7 @@ void FlameModModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject&
void FlameModModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) void FlameModModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
{ {
FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance); FlameMod::loadIndexedPackVersions(m, arr);
} }
auto FlameModModel::loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion auto FlameModModel::loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion
@ -65,7 +65,7 @@ void FlameResourcePackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJso
void FlameResourcePackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) void FlameResourcePackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
{ {
FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance); FlameMod::loadIndexedPackVersions(m, arr);
} }
bool FlameResourcePackModel::optedOut(const ModPlatform::IndexedVersion& ver) const bool FlameResourcePackModel::optedOut(const ModPlatform::IndexedVersion& ver) const
@ -93,7 +93,7 @@ void FlameTexturePackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJson
void FlameTexturePackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) void FlameTexturePackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
{ {
FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance); FlameMod::loadIndexedPackVersions(m, arr);
QVector<ModPlatform::IndexedVersion> filtered_versions(m.versions.size()); QVector<ModPlatform::IndexedVersion> filtered_versions(m.versions.size());
@ -157,7 +157,7 @@ void FlameShaderPackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonO
void FlameShaderPackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) void FlameShaderPackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
{ {
FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance); FlameMod::loadIndexedPackVersions(m, arr);
} }
bool FlameShaderPackModel::optedOut(const ModPlatform::IndexedVersion& ver) const bool FlameShaderPackModel::optedOut(const ModPlatform::IndexedVersion& ver) const

View file

@ -106,9 +106,6 @@ QVariant ListModel::data(const QModelIndex& index, int role) const
} }
auto pack = m_modpacks.at(pos); auto pack = m_modpacks.at(pos);
if (role == Qt::ToolTipRole) {
}
switch (role) { switch (role) {
case Qt::ToolTipRole: case Qt::ToolTipRole:
return tr("Minecraft %1").arg(pack.mcVersion); return tr("Minecraft %1").arg(pack.mcVersion);

View file

@ -213,7 +213,7 @@ void ListModel::fill(ModpackList modpacks_)
endResetModel(); endResetModel();
} }
void ListModel::addPack(Modpack modpack) void ListModel::addPack(const Modpack& modpack)
{ {
beginResetModel(); beginResetModel();
this->modpacks.append(modpack); this->modpacks.append(modpack);

View file

@ -62,7 +62,7 @@ class ListModel : public QAbstractListModel {
Qt::ItemFlags flags(const QModelIndex& index) const override; Qt::ItemFlags flags(const QModelIndex& index) const override;
void fill(ModpackList modpacks); void fill(ModpackList modpacks);
void addPack(Modpack modpack); void addPack(const Modpack& modpack);
void clear(); void clear();
void remove(int row); void remove(int row);

View file

@ -213,7 +213,7 @@ void Page::ftbPackDataDownloadAborted()
CustomMessageBox::selectable(this, tr("Task aborted"), tr("The task has been aborted by the user."), QMessageBox::Information)->show(); CustomMessageBox::selectable(this, tr("Task aborted"), tr("The task has been aborted by the user."), QMessageBox::Information)->show();
} }
void Page::ftbPrivatePackDataDownloadSuccessfully(Modpack pack) void Page::ftbPrivatePackDataDownloadSuccessfully(const Modpack& pack)
{ {
privateListModel->addPack(pack); privateListModel->addPack(pack);
} }

View file

@ -85,7 +85,7 @@ class Page : public QWidget, public ModpackProviderBasePage {
void ftbPackDataDownloadFailed(QString reason); void ftbPackDataDownloadFailed(QString reason);
void ftbPackDataDownloadAborted(); void ftbPackDataDownloadAborted();
void ftbPrivatePackDataDownloadSuccessfully(Modpack pack); void ftbPrivatePackDataDownloadSuccessfully(const Modpack& pack);
void ftbPrivatePackDataDownloadFailed(QString reason, QString packCode); void ftbPrivatePackDataDownloadFailed(QString reason, QString packCode);
void onSortingSelectionChanged(QString data); void onSortingSelectionChanged(QString data);

View file

@ -39,7 +39,7 @@ void ModrinthModModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObjec
void ModrinthModModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) void ModrinthModModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
{ {
::Modrinth::loadIndexedPackVersions(m, arr, &m_base_instance); ::Modrinth::loadIndexedPackVersions(m, arr);
} }
auto ModrinthModModel::loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion auto ModrinthModModel::loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion
@ -66,7 +66,7 @@ void ModrinthResourcePackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, Q
void ModrinthResourcePackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) void ModrinthResourcePackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
{ {
::Modrinth::loadIndexedPackVersions(m, arr, &m_base_instance); ::Modrinth::loadIndexedPackVersions(m, arr);
} }
auto ModrinthResourcePackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray auto ModrinthResourcePackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray
@ -88,7 +88,7 @@ void ModrinthTexturePackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJ
void ModrinthTexturePackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) void ModrinthTexturePackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
{ {
::Modrinth::loadIndexedPackVersions(m, arr, &m_base_instance); ::Modrinth::loadIndexedPackVersions(m, arr);
} }
auto ModrinthTexturePackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray auto ModrinthTexturePackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray
@ -110,7 +110,7 @@ void ModrinthShaderPackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJs
void ModrinthShaderPackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) void ModrinthShaderPackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
{ {
::Modrinth::loadIndexedPackVersions(m, arr, &m_base_instance); ::Modrinth::loadIndexedPackVersions(m, arr);
} }
auto ModrinthShaderPackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray auto ModrinthShaderPackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray

View file

@ -40,18 +40,29 @@
#include "HintOverrideProxyStyle.h" #include "HintOverrideProxyStyle.h"
#include "ThemeManager.h" #include "ThemeManager.h"
SystemTheme::SystemTheme(const QString& styleName, const QPalette& palette, bool isDefaultTheme) // See https://github.com/MultiMC/Launcher/issues/1790
// or https://github.com/PrismLauncher/PrismLauncher/issues/490
static const QStringList S_NATIVE_STYLES{ "windows11", "windowsvista", "macos", "system", "windows" };
SystemTheme::SystemTheme(const QString& styleName, const QPalette& defaultPalette, bool isDefaultTheme)
{ {
themeName = isDefaultTheme ? "system" : styleName; m_themeName = isDefaultTheme ? "system" : styleName;
widgetTheme = styleName; m_widgetTheme = styleName;
colorPalette = palette; // NOTE: SystemTheme is reconstructed on page refresh. We can't accurately determine the system palette here
// See also S_NATIVE_STYLES comment
if (S_NATIVE_STYLES.contains(m_themeName)) {
m_colorPalette = defaultPalette;
} else {
auto style = QStyleFactory::create(styleName);
m_colorPalette = style->standardPalette();
delete style;
}
} }
void SystemTheme::apply(bool initial) void SystemTheme::apply(bool initial)
{ {
// See https://github.com/MultiMC/Launcher/issues/1790 // See S_NATIVE_STYLES comment
// or https://github.com/PrismLauncher/PrismLauncher/issues/490 if (initial && S_NATIVE_STYLES.contains(m_themeName)) {
if (initial) {
QApplication::setStyle(new HintOverrideProxyStyle(QStyleFactory::create(qtTheme()))); QApplication::setStyle(new HintOverrideProxyStyle(QStyleFactory::create(qtTheme())));
return; return;
} }
@ -61,35 +72,35 @@ void SystemTheme::apply(bool initial)
QString SystemTheme::id() QString SystemTheme::id()
{ {
return themeName; return m_themeName;
} }
QString SystemTheme::name() QString SystemTheme::name()
{ {
if (themeName.toLower() == "windowsvista") { if (m_themeName.toLower() == "windowsvista") {
return QObject::tr("Windows Vista"); return QObject::tr("Windows Vista");
} else if (themeName.toLower() == "windows") { } else if (m_themeName.toLower() == "windows") {
return QObject::tr("Windows 9x"); return QObject::tr("Windows 9x");
} else if (themeName.toLower() == "windows11") { } else if (m_themeName.toLower() == "windows11") {
return QObject::tr("Windows 11"); return QObject::tr("Windows 11");
} else if (themeName.toLower() == "system") { } else if (m_themeName.toLower() == "system") {
return QObject::tr("System"); return QObject::tr("System");
} else { } else {
return themeName; return m_themeName;
} }
} }
QString SystemTheme::tooltip() QString SystemTheme::tooltip()
{ {
if (themeName.toLower() == "windowsvista") { if (m_themeName.toLower() == "windowsvista") {
return QObject::tr("Widget style trying to look like your win32 theme"); return QObject::tr("Widget style trying to look like your win32 theme");
} else if (themeName.toLower() == "windows") { } else if (m_themeName.toLower() == "windows") {
return QObject::tr("Windows 9x inspired widget style"); return QObject::tr("Windows 9x inspired widget style");
} else if (themeName.toLower() == "windows11") { } else if (m_themeName.toLower() == "windows11") {
return QObject::tr("WinUI 3 inspired Qt widget style"); return QObject::tr("WinUI 3 inspired Qt widget style");
} else if (themeName.toLower() == "fusion") { } else if (m_themeName.toLower() == "fusion") {
return QObject::tr("The default Qt widget style"); return QObject::tr("The default Qt widget style");
} else if (themeName.toLower() == "system") { } else if (m_themeName.toLower() == "system") {
return QObject::tr("Your current system theme"); return QObject::tr("Your current system theme");
} else { } else {
return ""; return "";
@ -98,12 +109,12 @@ QString SystemTheme::tooltip()
QString SystemTheme::qtTheme() QString SystemTheme::qtTheme()
{ {
return widgetTheme; return m_widgetTheme;
} }
QPalette SystemTheme::colorScheme() QPalette SystemTheme::colorScheme()
{ {
return colorPalette; return m_colorPalette;
} }
QString SystemTheme::appStyleSheet() QString SystemTheme::appStyleSheet()

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