aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/ISSUE_TEMPLATE/bug_report.yml4
-rw-r--r--.github/ISSUE_TEMPLATE/rfc.yml5
-rw-r--r--.github/ISSUE_TEMPLATE/suggestion.yml2
-rw-r--r--.github/workflows/backport.yml4
-rw-r--r--.github/workflows/build.yml50
-rw-r--r--.github/workflows/pr-comment.yml61
-rw-r--r--.github/workflows/trigger_builds.yml10
-rw-r--r--.github/workflows/trigger_release.yml93
-rw-r--r--.gitignore4
-rw-r--r--CMakeLists.txt8
-rw-r--r--README.md20
-rw-r--r--cmake/UnitTest.cmake66
-rw-r--r--launcher/Application.cpp10
-rw-r--r--launcher/Application.h2
-rw-r--r--launcher/CMakeLists.txt53
-rw-r--r--launcher/DesktopServices.cpp82
-rw-r--r--launcher/java/JavaUtils.cpp4
-rw-r--r--launcher/launch/LaunchTask.cpp44
-rw-r--r--launcher/launch/steps/CheckJava.cpp44
-rw-r--r--launcher/launch/steps/QuitAfterGameStop.cpp26
-rw-r--r--launcher/launch/steps/QuitAfterGameStop.h35
-rw-r--r--launcher/minecraft/MinecraftInstance.cpp9
-rw-r--r--launcher/minecraft/World.cpp23
-rw-r--r--launcher/minecraft/World.h5
-rw-r--r--launcher/minecraft/WorldList.cpp18
-rw-r--r--launcher/minecraft/WorldList.h4
-rw-r--r--launcher/minecraft/launch/LauncherPartLaunch.cpp1
-rw-r--r--launcher/modplatform/ModAPI.h38
-rw-r--r--launcher/modplatform/ModIndex.h42
-rw-r--r--launcher/modplatform/flame/FlameAPI.h32
-rw-r--r--launcher/modplatform/flame/FlameModIndex.cpp62
-rw-r--r--launcher/modplatform/flame/FlameModIndex.h50
-rw-r--r--launcher/modplatform/helpers/NetworkModAPI.cpp60
-rw-r--r--launcher/modplatform/helpers/NetworkModAPI.h13
-rw-r--r--launcher/modplatform/modrinth/ModrinthAPI.h73
-rw-r--r--launcher/modplatform/modrinth/ModrinthPackIndex.cpp65
-rw-r--r--launcher/modplatform/modrinth/ModrinthPackIndex.h50
-rw-r--r--launcher/resources/pe_light/scalable/launcher.svg18
-rw-r--r--launcher/ui/dialogs/AboutDialog.cpp78
-rw-r--r--launcher/ui/dialogs/AboutDialog.ui9
-rw-r--r--launcher/ui/pages/global/AccountListPage.cpp7
-rw-r--r--launcher/ui/pages/global/LauncherPage.cpp23
-rw-r--r--launcher/ui/pages/global/MinecraftPage.cpp2
-rw-r--r--launcher/ui/pages/global/MinecraftPage.ui10
-rw-r--r--launcher/ui/pages/instance/VersionPage.cpp3
-rw-r--r--launcher/ui/pages/modplatform/ModModel.cpp234
-rw-r--r--launcher/ui/pages/modplatform/ModModel.h85
-rw-r--r--launcher/ui/pages/modplatform/ModPage.cpp170
-rw-r--r--launcher/ui/pages/modplatform/ModPage.h69
-rw-r--r--launcher/ui/pages/modplatform/ModPage.ui (renamed from launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui)4
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameModModel.cpp268
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameModModel.h78
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameModPage.cpp240
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameModPage.h72
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameModPage.ui97
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp289
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthModel.h80
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp227
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthPage.h72
-rw-r--r--program_info/LICENSE57
60 files changed, 1684 insertions, 1680 deletions
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index eb560f0e..1ede3f74 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -1,6 +1,6 @@
name: Bug Report
description: File a bug report
-labels: [bug, needs-triage]
+labels: [bug]
body:
- type: markdown
attributes:
@@ -8,7 +8,7 @@ body:
If you need help with running Minecraft, please visit us on our Discord before making a bug report.
Before submitting a bug report, please make sure you have read this *entire* form, and that:
- * You have read the [FAQ](https://github.com/PolyMC/PolyMC/wiki/FAQ) and it has not answered your question
+ * You have read the [PolyMC wiki](https://polymc.org/wiki/) and it has not answered your question.
* Your bug is not caused by Minecraft or any mods you have installed.
* Your issue has not been reported before, [make sure to use the search function!](https://github.com/PolyMC/PolyMC/issues)
diff --git a/.github/ISSUE_TEMPLATE/rfc.yml b/.github/ISSUE_TEMPLATE/rfc.yml
index 664430fe..0a40d01d 100644
--- a/.github/ISSUE_TEMPLATE/rfc.yml
+++ b/.github/ISSUE_TEMPLATE/rfc.yml
@@ -1,7 +1,7 @@
# Template based on https://gitlab.archlinux.org/archlinux/rfcs/-/blob/0ba3b61e987e197f8d1901709409b8564958f78a/rfcs/0000-template.rst
name: Request for Comment (RFC)
description: Propose a larger change and start a discussion.
-labels: [RFC]
+labels: [rfc]
body:
- type: markdown
attributes:
@@ -21,8 +21,7 @@ body:
Introduce the topic. If this is a not-well-known section of PolyMC, a detailed explanation of the background is recommended.
Some example points of discussion:
- What specific problems are you facing right now that you're trying to address?
- - Are there any previous discussions? Link to them and summarize them (don't
- - force your readers to read them though!).
+ - Are there any previous discussions? Link to them and summarize them (don't force your readers to read them though!).
- Is there any precedent set by other software? If so, link to resources.
placeholder: I don't like cats. I think many users also don't like cats.
validations:
diff --git a/.github/ISSUE_TEMPLATE/suggestion.yml b/.github/ISSUE_TEMPLATE/suggestion.yml
index b58a6672..48f157b3 100644
--- a/.github/ISSUE_TEMPLATE/suggestion.yml
+++ b/.github/ISSUE_TEMPLATE/suggestion.yml
@@ -1,6 +1,6 @@
name: Suggestion
description: Make a suggestion
-labels: [idea, needs-triage]
+labels: [enhancement]
body:
- type: markdown
attributes:
diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml
index e1a7cffa..fa287a2c 100644
--- a/.github/workflows/backport.yml
+++ b/.github/workflows/backport.yml
@@ -9,7 +9,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: checkout
- uses: actions/checkout@v1
+ uses: actions/checkout@v3
+ with:
+ fetch-depth: 0
- name: Backport PR by cherry-pick-ing
uses: Nathanmalnoury/gh-backport-action@master
with:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 4aa316f4..b011a779 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -20,6 +20,12 @@ jobs:
qt_host: linux
- os: ubuntu-20.04
+ name: Linux-Portable
+ qt_version: 5.12.8
+ qt_host: linux
+ portable: true
+
+ - os: ubuntu-20.04
qt_version: 5.15.2
qt_host: linux
app_image: true
@@ -58,7 +64,7 @@ jobs:
steps:
- name: Checkout
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
with:
submodules: 'true'
@@ -91,7 +97,7 @@ jobs:
- name: Cache Qt
if: runner.os != 'Windows'
id: cache-qt
- uses: actions/cache@v2
+ uses: actions/cache@v3
with:
path: "${{ github.workspace }}/Qt/"
key: ${{ runner.os }}-${{ matrix.qt_version }}-${{ matrix.qt_arch }}-qt_cache
@@ -140,7 +146,6 @@ jobs:
run: |
cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DLauncher_PORTABLE=OFF -G Ninja
-
- name: Configure CMake on Windows portable
if: runner.os == 'Windows' && matrix.portable == true
shell: msys2 {0}
@@ -148,10 +153,15 @@ jobs:
cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -G Ninja
- name: Configure CMake on Linux
- if: runner.os == 'Linux'
+ if: runner.os == 'Linux' && matrix.portable != true
run: |
cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DLauncher_PORTABLE=OFF -G Ninja
+ - name: Configure CMake on Linux Portable
+ if: runner.os == 'Linux' && matrix.portable == true
+ run: |
+ cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -G Ninja
+
- name: Build
if: runner.os != 'Windows'
run: |
@@ -175,10 +185,15 @@ jobs:
cmake --install ${{ env.BUILD_DIR }}
- name: Install on Linux
- if: runner.os == 'Linux'
+ if: runner.os == 'Linux' && matrix.portable != true
run: |
DESTDIR=${{ env.INSTALL_DIR }} cmake --install ${{ env.BUILD_DIR }}
+ - name: Install on Linux portable
+ if: runner.os == 'Linux' && matrix.portable == true
+ run: |
+ cmake --install ${{ env.BUILD_DIR }}
+
- name: Bundle AppImage
if: matrix.app_image == true
shell: bash
@@ -219,21 +234,34 @@ jobs:
tar -czf ../PolyMC.tar.gz *
- name: tar on Linux
- if: runner.os == 'Linux' && matrix.app_image != true
+ if: runner.os == 'Linux' && matrix.app_image != true && matrix.portable != true
run: |
cd ${{ env.INSTALL_DIR }}
tar -czf ../PolyMC.tar.gz *
+ - name: tar on Linux portable
+ if: runner.os == 'Linux' && matrix.app_image != true && matrix.portable == true
+ run: |
+ cd ${{ env.INSTALL_DIR }}
+ tar -czf ../PolyMC-portable.tar.gz *
+
- name: Upload Linux tar.gz
- if: runner.os == 'Linux' && matrix.app_image != true
- uses: actions/upload-artifact@v2
+ if: runner.os == 'Linux' && matrix.app_image != true && matrix.portable != true
+ uses: actions/upload-artifact@v3
with:
name: PolyMC-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}
path: PolyMC.tar.gz
+ - name: Upload Linux Portable tar.gz
+ if: runner.os == 'Linux' && matrix.app_image != true && matrix.portable == true
+ uses: actions/upload-artifact@v3
+ with:
+ name: PolyMC-${{ runner.os }}-Portable-${{ env.VERSION }}-${{ inputs.build_type }}
+ path: PolyMC-portable.tar.gz
+
- name: Upload AppImage for Linux
if: matrix.app_image == true
- uses: actions/upload-artifact@v2
+ uses: actions/upload-artifact@v3
with:
name: PolyMC-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage
path: PolyMC-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage
@@ -254,14 +282,14 @@ jobs:
- name: Upload package for Windows
if: runner.os == 'Windows'
- uses: actions/upload-artifact@v2
+ uses: actions/upload-artifact@v3
with:
name: PolyMC-${{ matrix.name }}-${{ env.VERSION }}-${{ inputs.build_type }}
path: ${{ env.INSTALL_DIR }}/**
- name: Upload package for macOS
if: runner.os == 'macOS'
- uses: actions/upload-artifact@v2
+ uses: actions/upload-artifact@v3
with:
name: PolyMC-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}
path: PolyMC.tar.gz
diff --git a/.github/workflows/pr-comment.yml b/.github/workflows/pr-comment.yml
new file mode 100644
index 00000000..7e8e4d99
--- /dev/null
+++ b/.github/workflows/pr-comment.yml
@@ -0,0 +1,61 @@
+name: Comment on pull request
+on:
+ workflow_run:
+ workflows: ['Test workflow with upload']
+ types: [completed]
+jobs:
+ pr_comment:
+ if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success'
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/github-script@v5
+ with:
+ # This snippet is public-domain, taken from
+ # https://github.com/oprypin/nightly.link/blob/master/.github/workflows/pr-comment.yml
+ script: |
+ async function upsertComment(owner, repo, issue_number, purpose, body) {
+ const {data: comments} = await github.rest.issues.listComments(
+ {owner, repo, issue_number});
+
+ const marker = `<!-- bot: ${purpose} -->`;
+ body = marker + "\n" + body;
+
+ const existing = comments.filter((c) => c.body.includes(marker));
+ if (existing.length > 0) {
+ const last = existing[existing.length - 1];
+ core.info(`Updating comment ${last.id}`);
+ await github.rest.issues.updateComment({
+ owner, repo,
+ body,
+ comment_id: last.id,
+ });
+ } else {
+ core.info(`Creating a comment in issue / PR #${issue_number}`);
+ await github.rest.issues.createComment({issue_number, body, owner, repo});
+ }
+ }
+
+ const {owner, repo} = context.repo;
+ const run_id = ${{github.event.workflow_run.id}};
+
+ const pull_requests = ${{ toJSON(github.event.workflow_run.pull_requests) }};
+ if (!pull_requests.length) {
+ return core.error("This workflow doesn't match any pull requests!");
+ }
+
+ const artifacts = await github.paginate(
+ github.rest.actions.listWorkflowRunArtifacts, {owner, repo, run_id});
+ if (!artifacts.length) {
+ return core.error(`No artifacts found`);
+ }
+ let body = `Download the artifacts for this pull request:\n`;
+ for (const art of artifacts) {
+ body += `\n* [${art.name}.zip](https://nightly.link/${owner}/${repo}/actions/artifacts/${art.id}.zip)`;
+ }
+
+ core.info("Review thread message body:", body);
+
+ for (const pr of pull_requests) {
+ await upsertComment(owner, repo, pr.number,
+ "nightly-link", body);
+ }
diff --git a/.github/workflows/trigger_builds.yml b/.github/workflows/trigger_builds.yml
index 1561b9d6..3ec6bb95 100644
--- a/.github/workflows/trigger_builds.yml
+++ b/.github/workflows/trigger_builds.yml
@@ -9,12 +9,16 @@ on:
- '**/LICENSE'
- 'flake.lock'
- '**.nix'
+ - 'packages/**'
+ - '.github/ISSUE_TEMPLATE/**'
pull_request:
paths-ignore:
- '**.md'
- '**/LICENSE'
- 'flake.lock'
- '**.nix'
+ - 'packages/**'
+ - '.github/ISSUE_TEMPLATE/**'
workflow_dispatch:
jobs:
@@ -24,9 +28,3 @@ jobs:
uses: ./.github/workflows/build.yml
with:
build_type: Debug
-
- build_release:
- name: Build Release
- uses: ./.github/workflows/build.yml
- with:
- build_type: Release
diff --git a/.github/workflows/trigger_release.yml b/.github/workflows/trigger_release.yml
index b487e731..149cb632 100644
--- a/.github/workflows/trigger_release.yml
+++ b/.github/workflows/trigger_release.yml
@@ -19,81 +19,52 @@ jobs:
outputs:
upload_url: ${{ steps.create_release.outputs.upload_url }}
steps:
- - name: Grab and store version
- run: |
- tag_name=$(echo ${{ github.ref }} | grep -oE "[^/]+$")
- echo "VERSION=$tag_name" >> $GITHUB_ENV
- - name: Create release
- id: create_release
- uses: softprops/action-gh-release@v1
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ - name: Checkout
+ uses: actions/checkout@v3
with:
- tag_name: ${{ github.ref }}
- name: PolyMC ${{ env.VERSION }}
- draft: true
- prerelease: false
-
- upload_release:
- needs: create_release
- runs-on: ubuntu-latest
- steps:
-
+ submodules: 'true'
+ path: 'PolyMC-source'
- name: Download artifacts
- uses: actions/download-artifact@v2
-
+ uses: actions/download-artifact@v3
- name: Grab and store version
run: |
tag_name=$(echo ${{ github.ref }} | grep -oE "[^/]+$")
echo "VERSION=$tag_name" >> $GITHUB_ENV
-
- name: Package artifacts properly
run: |
+ mv ${{ github.workspace }}/PolyMC-source PolyMC-${{ env.VERSION }}
mv PolyMC-Linux*/PolyMC.tar.gz PolyMC-Linux-${{ env.VERSION }}.tar.gz
mv PolyMC-*.AppImage/PolyMC-*.AppImage PolyMC-Linux-${{ env.VERSION }}-x86_64.AppImage
- mv PolyMC-Windows* PolyMC-Windows-${{ env.VERSION }}
mv PolyMC-macOS*/PolyMC.tar.gz PolyMC-macOS-${{ env.VERSION }}.tar.gz
- cd PolyMC-Windows-${{ env.VERSION }}
- zip -r -9 ../PolyMC-Windows-${{ env.VERSION }}.zip *
- cd ..
+ tar -czf PolyMC-${{ env.VERSION }}.tar.gz PolyMC-${{ env.VERSION }}
- - name: Upload Linux asset
- uses: actions/upload-release-asset@v1
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- with:
- upload_url: ${{ needs.create_release.outputs.upload_url }}
- asset_name: PolyMC-Linux-${{ env.VERSION }}.tar.gz
- asset_path: PolyMC-Linux-${{ env.VERSION }}.tar.gz
- asset_content_type: application/gzip
-
- - name: Upload Linux AppImage asset
- uses: actions/upload-release-asset@v1
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- with:
- upload_url: ${{ needs.create_release.outputs.upload_url }}
- asset_name: PolyMC-Linux-${{ env.VERSION }}-x86_64.AppImage
- asset_path: PolyMC-Linux-${{ env.VERSION }}-x86_64.AppImage
- asset_content_type: application/x-executable
-
- - name: Upload Windows asset
- uses: actions/upload-release-asset@v1
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- with:
- upload_url: ${{ needs.create_release.outputs.upload_url }}
- asset_name: PolyMC-Windows-${{ env.VERSION }}.zip
- asset_path: PolyMC-Windows-${{ env.VERSION }}.zip
- asset_content_type: application/zip
+ for d in PolyMC-Windows-*; do
+ cd "${d}" || continue
+ ARCH="$(echo -n ${d} | cut -d '-' -f 3)"
+ PORT="$(echo -n ${d} | grep -o portable || true)"
+ NAME="PolyMC-Windows-${ARCH}"
+ test -z "${PORT}" || NAME="${NAME}-portable"
+ zip -r -9 "../${NAME}-${{ env.VERSION }}.zip" *
+ cd ..
+ done
- - name: Upload macOS asset
- uses: actions/upload-release-asset@v1
+ - name: Create release
+ id: create_release
+ uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
- upload_url: ${{ needs.create_release.outputs.upload_url }}
- asset_name: PolyMC-macOS-${{ env.VERSION }}.tar.gz
- asset_path: PolyMC-macOS-${{ env.VERSION }}.tar.gz
- asset_content_type: application/gzip
+ tag_name: ${{ github.ref }}
+ name: PolyMC ${{ env.VERSION }}
+ draft: true
+ prerelease: false
+ files: |
+ PolyMC-Linux-${{ env.VERSION }}.tar.gz
+ PolyMC-Linux-${{ env.VERSION }}-x86_64.AppImage
+ PolyMC-Windows-i686-${{ env.VERSION }}.zip
+ PolyMC-Windows-i686-portable-${{ env.VERSION }}.zip
+ PolyMC-Windows-x86_64-${{ env.VERSION }}.zip
+ PolyMC-Windows-x86_64-portable-${{ env.VERSION }}.zip
+ PolyMC-macOS-${{ env.VERSION }}.tar.gz
+ PolyMC-${{ env.VERSION }}.tar.gz
diff --git a/.gitignore b/.gitignore
index ba90e8f8..c9a762f5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -42,3 +42,7 @@ run/
# Nix/NixOS
result/
+
+# Flatpak
+.flatpak-builder
+flatbuild
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 7537703c..d45d4975 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -6,7 +6,7 @@ if(WIN32)
endif()
project(Launcher)
-enable_testing()
+include(CTest)
string(COMPARE EQUAL "${CMAKE_SOURCE_DIR}" "${CMAKE_BUILD_DIR}" IS_IN_SOURCE_BUILD)
if(IS_IN_SOURCE_BUILD)
@@ -53,7 +53,7 @@ set(Launcher_HELP_URL "https://polymc.org/wiki/%1" CACHE STRING "URL (with arg %
######## Set version numbers ########
set(Launcher_VERSION_MAJOR 1)
set(Launcher_VERSION_MINOR 1)
-set(Launcher_VERSION_HOTFIX 0)
+set(Launcher_VERSION_HOTFIX 1)
# Build number
set(Launcher_VERSION_BUILD -1 CACHE STRING "Build number. -1 for no build number.")
@@ -80,7 +80,7 @@ set(Launcher_BUG_TRACKER_URL "https://github.com/PolyMC/PolyMC/issues" CACHE STR
set(Launcher_TRANSLATIONS_URL "https://hosted.weblate.org/projects/polymc/polymc/" CACHE STRING "URL for the translations platform.")
# Matrix Space
-set(Launcher_MATRIX_URL "https://matrix.to/#/#polymc:polymc.org" CACHE STRING "URL to the Matrix Space")
+set(Launcher_MATRIX_URL "https://matrix.to/#/#polymc:matrix.org" CACHE STRING "URL to the Matrix Space")
# Discord URL
set(Launcher_DISCORD_URL "https://discord.gg/Z52pwxWCHP" CACHE STRING "URL for the Discord guild.")
@@ -88,7 +88,7 @@ set(Launcher_DISCORD_URL "https://discord.gg/Z52pwxWCHP" CACHE STRING "URL for t
# Subreddit URL
-set(Launcher_SUBREDDIT_URL "" CACHE STRING "URL for the subreddit.")
+set(Launcher_SUBREDDIT_URL "https://www.reddit.com/r/PolyMCLauncher/" CACHE STRING "URL for the subreddit.")
# Builds
# TODO: Launcher_FORCE_BUNDLED_LIBS should be off in the future, but as of QuaZip 1.2, we can't do that yet.
diff --git a/README.md b/README.md
index a114869c..4c73c47e 100644
--- a/README.md
+++ b/README.md
@@ -33,14 +33,22 @@ Feel free to create an issue if you need help. However, you might find it easier
For people who don't want to use Discord, we have a Matrix Space which is bridged to the Discord server:
-[![PolyMC Space](https://img.shields.io/matrix/polymc:polymc.org?label=PolyMC%20Space&server_fqdn=matrix.polymc.org)](https://matrix.to/#/#polymc:polymc.org)
+[![PolyMC Space](https://img.shields.io/matrix/polymc:matrix.org?label=PolyMC%20space)](https://matrix.to/#/#polymc:matrix.org)
If there are any issues with the space or you are using a client that does not support the feature here are the individual rooms:
-[![Support](https://img.shields.io/matrix/support:polymc.org?label=%23support&server_fqdn=matrix.polymc.org)](https://matrix.to/#/#support:polymc.org)
-[![Discussion](https://img.shields.io/matrix/discussion:polymc.org?label=%23discussion&server_fqdn=matrix.polymc.org)](https://matrix.to/#/#discussion:polymc.org)
-[![Development](https://img.shields.io/matrix/development:polymc.org?label=%23development&server_fqdn=matrix.polymc.org)](https://matrix.to/#/#development:polymc.org)
-[![News](https://img.shields.io/matrix/news:polymc.org?label=%23news&server_fqdn=matrix.polymc.org)](https://matrix.to/#/#news:polymc.org)
+[![Development](https://img.shields.io/matrix/polymc-development:matrix.org?label=PolyMC%20Development)](https://matrix.to/#/#polymc-development:matrix.org)
+[![Discussion](https://img.shields.io/matrix/polymc-discussion:matrix.org?label=PolyMC%20Discussion)](https://matrix.to/#/#polymc-discussion:matrix.org)
+[![Github](https://img.shields.io/matrix/polymc-github:matrix.org?label=PolyMC%20Github)](https://matrix.to/#/#polymc-github:matrix.org)
+[![Maintainers](https://img.shields.io/matrix/polymc-maintainers:matrix.org?label=PolyMC%20Maintainers)](https://matrix.to/#/#polymc-maintainers:matrix.org)
+[![News](https://img.shields.io/matrix/polymc-news:matrix.org?label=PolyMC%20News)](https://matrix.to/#/#polymc-news:matrix.org)
+[![Offtopic](https://img.shields.io/matrix/polymc-offtopic:matrix.org?label=PolyMC%20Offtopic)](https://matrix.to/#/#polymc-offtopic:matrix.org)
+[![Support](https://img.shields.io/matrix/polymc-support:matrix.org?label=PolyMC%20Support)](https://matrix.to/#/#polymc-support:matrix.org)
+[![Voice](https://img.shields.io/matrix/polymc-voice:matrix.org?label=PolyMC%20Voice)](https://matrix.to/#/#polymc-voice:matrix.org)
+
+we also have a subreddit you can post your issues and suggestions on:
+
+[r/PolyMCLauncher](https://www.reddit.com/r/PolyMCLauncher/)
# Development
@@ -77,4 +85,4 @@ All launcher code is available under the GPL-3 license.
[Source for the website](https://github.com/PolyMC/polymc.github.io) is hosted under the AGPL-3 License.
-The logo and related assets are under the CC BY-NC-SA 4.0 license.
+The logo and related assets are under the CC BY-SA 4.0 license.
diff --git a/cmake/UnitTest.cmake b/cmake/UnitTest.cmake
index 9f2bc269..7d7bd4ad 100644
--- a/cmake/UnitTest.cmake
+++ b/cmake/UnitTest.cmake
@@ -5,44 +5,46 @@ set(TEST_RESOURCE_PATH ${CMAKE_CURRENT_LIST_DIR})
message(${TEST_RESOURCE_PATH})
function(add_unit_test name)
- set(options "")
- set(oneValueArgs DATA)
- set(multiValueArgs SOURCES LIBS)
+ if(BUILD_TESTING)
+ set(options "")
+ set(oneValueArgs DATA)
+ set(multiValueArgs SOURCES LIBS)
- cmake_parse_arguments(OPT "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN} )
+ cmake_parse_arguments(OPT "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN} )
- if(WIN32)
- add_executable(${name}_test ${OPT_SOURCES} ${TEST_RESOURCE_PATH}/UnitTest/test.rc)
- else()
- add_executable(${name}_test ${OPT_SOURCES})
- endif()
-
- if(NOT "${OPT_DATA}" STREQUAL "")
- set(TEST_DATA_PATH "${CMAKE_CURRENT_BINARY_DIR}/data")
- set(TEST_DATA_PATH_SRC "${CMAKE_CURRENT_SOURCE_DIR}/${OPT_DATA}")
- message("From ${TEST_DATA_PATH_SRC} to ${TEST_DATA_PATH}")
- string(REGEX REPLACE "[/\\:]" "_" DATA_TARGET_NAME "${TEST_DATA_PATH_SRC}")
- if(UNIX)
- # on unix we get the third / from the filename
- set(TEST_DATA_URL "file://${TEST_DATA_PATH}")
+ if(WIN32)
+ add_executable(${name}_test ${OPT_SOURCES} ${TEST_RESOURCE_PATH}/UnitTest/test.rc)
else()
- # we don't on windows, so we have to add it ourselves
- set(TEST_DATA_URL "file:///${TEST_DATA_PATH}")
+ add_executable(${name}_test ${OPT_SOURCES})
endif()
- if(NOT TARGET "${DATA_TARGET_NAME}")
- add_custom_target(${DATA_TARGET_NAME})
- add_dependencies(${name}_test ${DATA_TARGET_NAME})
- add_custom_command(
- TARGET ${DATA_TARGET_NAME}
- COMMAND ${CMAKE_COMMAND} "-DTEST_DATA_URL=${TEST_DATA_URL}" -DSOURCE=${TEST_DATA_PATH_SRC} -DDESTINATION=${TEST_DATA_PATH} -P ${TEST_RESOURCE_PATH}/UnitTest/generate_test_data.cmake
- WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
- )
+
+ if(NOT "${OPT_DATA}" STREQUAL "")
+ set(TEST_DATA_PATH "${CMAKE_CURRENT_BINARY_DIR}/data")
+ set(TEST_DATA_PATH_SRC "${CMAKE_CURRENT_SOURCE_DIR}/${OPT_DATA}")
+ message("From ${TEST_DATA_PATH_SRC} to ${TEST_DATA_PATH}")
+ string(REGEX REPLACE "[/\\:]" "_" DATA_TARGET_NAME "${TEST_DATA_PATH_SRC}")
+ if(UNIX)
+ # on unix we get the third / from the filename
+ set(TEST_DATA_URL "file://${TEST_DATA_PATH}")
+ else()
+ # we don't on windows, so we have to add it ourselves
+ set(TEST_DATA_URL "file:///${TEST_DATA_PATH}")
+ endif()
+ if(NOT TARGET "${DATA_TARGET_NAME}")
+ add_custom_target(${DATA_TARGET_NAME})
+ add_dependencies(${name}_test ${DATA_TARGET_NAME})
+ add_custom_command(
+ TARGET ${DATA_TARGET_NAME}
+ COMMAND ${CMAKE_COMMAND} "-DTEST_DATA_URL=${TEST_DATA_URL}" -DSOURCE=${TEST_DATA_PATH_SRC} -DDESTINATION=${TEST_DATA_PATH} -P ${TEST_RESOURCE_PATH}/UnitTest/generate_test_data.cmake
+ WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
+ )
+ endif()
endif()
- endif()
- target_link_libraries(${name}_test Qt5::Test ${OPT_LIBS})
+ target_link_libraries(${name}_test Qt5::Test ${OPT_LIBS})
- target_include_directories(${name}_test PRIVATE "${TEST_RESOURCE_PATH}/UnitTest/")
+ target_include_directories(${name}_test PRIVATE "${TEST_RESOURCE_PATH}/UnitTest/")
- add_test(NAME ${name} COMMAND ${name}_test WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})
+ add_test(NAME ${name} COMMAND ${name}_test WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})
+ endif()
endfunction()
diff --git a/launcher/Application.cpp b/launcher/Application.cpp
index 0d769192..690a7ee4 100644
--- a/launcher/Application.cpp
+++ b/launcher/Application.cpp
@@ -728,6 +728,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
m_settings->registerSetting("PastebinURL", "https://0x0.st");
m_settings->registerSetting("CloseAfterLaunch", false);
+ m_settings->registerSetting("QuitAfterGameStop", false);
// Custom MSA credentials
m_settings->registerSetting("MSAClientIDOverride", "");
@@ -1140,6 +1141,15 @@ std::vector<ITheme *> Application::getValidApplicationThemes()
return ret;
}
+bool Application::isFlatpak()
+{
+ #ifdef Q_OS_LINUX
+ return QFile::exists("/.flatpak-info");
+ #else
+ return false;
+ #endif
+}
+
void Application::setApplicationTheme(const QString& name, bool initial)
{
auto systemPalette = qApp->palette();
diff --git a/launcher/Application.h b/launcher/Application.h
index c3e29ef5..54d9ba5f 100644
--- a/launcher/Application.h
+++ b/launcher/Application.h
@@ -104,6 +104,8 @@ public:
QIcon getThemedIcon(const QString& name);
+ bool isFlatpak();
+
void setIconTheme(const QString& name);
std::vector<ITheme *> getValidApplicationThemes();
diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt
index 98cb0a3b..692aebe5 100644
--- a/launcher/CMakeLists.txt
+++ b/launcher/CMakeLists.txt
@@ -144,6 +144,8 @@ set(LAUNCH_SOURCES
launch/steps/TextPrint.h
launch/steps/Update.cpp
launch/steps/Update.h
+ launch/steps/QuitAfterGameStop.cpp
+ launch/steps/QuitAfterGameStop.h
launch/LaunchStep.cpp
launch/LaunchStep.h
launch/LaunchTask.cpp
@@ -353,21 +355,23 @@ add_unit_test(GradleSpecifier
LIBS Launcher_logic
)
-add_executable(PackageManifest
- mojang/PackageManifest_test.cpp
-)
-target_link_libraries(PackageManifest
- Launcher_logic
- Qt5::Test
-)
-target_include_directories(PackageManifest
- PRIVATE ../cmake/UnitTest/
-)
-add_test(
- NAME PackageManifest
- COMMAND PackageManifest
- WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
-)
+if(BUILD_TESTING)
+ add_executable(PackageManifest
+ mojang/PackageManifest_test.cpp
+ )
+ target_link_libraries(PackageManifest
+ Launcher_logic
+ Qt5::Test
+ )
+ target_include_directories(PackageManifest
+ PRIVATE ../cmake/UnitTest/
+ )
+ add_test(
+ NAME PackageManifest
+ COMMAND PackageManifest
+ WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
+ )
+endif()
add_unit_test(MojangVersionFormat
SOURCES minecraft/MojangVersionFormat_test.cpp
@@ -485,6 +489,16 @@ set(META_SOURCES
meta/Index.h
)
+set(API_SOURCES
+ modplatform/ModAPI.h
+
+ modplatform/flame/FlameAPI.h
+ modplatform/modrinth/ModrinthAPI.h
+
+ modplatform/helpers/NetworkModAPI.h
+ modplatform/helpers/NetworkModAPI.cpp
+)
+
set(FTB_SOURCES
modplatform/legacy_ftb/PackFetchTask.h
modplatform/legacy_ftb/PackFetchTask.cpp
@@ -564,6 +578,7 @@ set(LOGIC_SOURCES
${TOOLS_SOURCES}
${META_SOURCES}
${ICONS_SOURCES}
+ ${API_SOURCES}
${FTB_SOURCES}
${FLAME_SOURCES}
${MODRINTH_SOURCES}
@@ -713,6 +728,11 @@ SET(LAUNCHER_SOURCES
ui/pages/modplatform/VanillaPage.cpp
ui/pages/modplatform/VanillaPage.h
+ ui/pages/modplatform/ModPage.cpp
+ ui/pages/modplatform/ModPage.h
+ ui/pages/modplatform/ModModel.cpp
+ ui/pages/modplatform/ModModel.h
+
ui/pages/modplatform/atlauncher/AtlFilterModel.cpp
ui/pages/modplatform/atlauncher/AtlFilterModel.h
ui/pages/modplatform/atlauncher/AtlListModel.cpp
@@ -871,13 +891,12 @@ qt5_wrap_ui(LAUNCHER_UI
ui/pages/modplatform/atlauncher/AtlOptionalModDialog.ui
ui/pages/modplatform/atlauncher/AtlPage.ui
ui/pages/modplatform/VanillaPage.ui
+ ui/pages/modplatform/ModPage.ui
ui/pages/modplatform/flame/FlamePage.ui
- ui/pages/modplatform/flame/FlameModPage.ui
ui/pages/modplatform/legacy_ftb/Page.ui
ui/pages/modplatform/ImportPage.ui
ui/pages/modplatform/ftb/FtbPage.ui
ui/pages/modplatform/technic/TechnicPage.ui
- ui/pages/modplatform/modrinth/ModrinthPage.ui
ui/widgets/InstanceCardWidget.ui
ui/widgets/CustomCommands.ui
ui/widgets/MCModInfoFrame.ui
diff --git a/launcher/DesktopServices.cpp b/launcher/DesktopServices.cpp
index dcc1b0ce..c29cbe7d 100644
--- a/launcher/DesktopServices.cpp
+++ b/launcher/DesktopServices.cpp
@@ -1,8 +1,43 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * PolyMC - Minecraft Launcher
+ * Copyright (C) 2022 dada513 <dada513@protonmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2022 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 "DesktopServices.h"
#include <QDir>
#include <QDesktopServices>
#include <QProcess>
#include <QDebug>
+#include "Application.h"
/**
* This shouldn't exist, but until QTBUG-9328 and other unreported bugs are fixed, it needs to be a thing.
@@ -84,7 +119,14 @@ bool openDirectory(const QString &path, bool ensureExists)
return QDesktopServices::openUrl(QUrl::fromLocalFile(dir.absolutePath()));
};
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
- return IndirectOpen(f);
+ if(!APPLICATION->isFlatpak())
+ {
+ return IndirectOpen(f);
+ }
+ else
+ {
+ return f();
+ }
#else
return f();
#endif
@@ -98,7 +140,14 @@ bool openFile(const QString &path)
return QDesktopServices::openUrl(QUrl::fromLocalFile(path));
};
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
- return IndirectOpen(f);
+ if(!APPLICATION->isFlatpak())
+ {
+ return IndirectOpen(f);
+ }
+ else
+ {
+ return f();
+ }
#else
return f();
#endif
@@ -109,10 +158,17 @@ bool openFile(const QString &application, const QString &path, const QString &wo
qDebug() << "Opening file" << path << "using" << application;
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
// FIXME: the pid here is fake. So if something depends on it, it will likely misbehave
- return IndirectOpen([&]()
+ if(!APPLICATION->isFlatpak())
{
- return QProcess::startDetached(application, QStringList() << path, workingDirectory);
- }, pid);
+ return IndirectOpen([&]()
+ {
+ return QProcess::startDetached(application, QStringList() << path, workingDirectory);
+ }, pid);
+ }
+ else
+ {
+ return QProcess::startDetached(application, QStringList() << path, workingDirectory, pid);
+ }
#else
return QProcess::startDetached(application, QStringList() << path, workingDirectory, pid);
#endif
@@ -122,11 +178,18 @@ bool run(const QString &application, const QStringList &args, const QString &wor
{
qDebug() << "Running" << application << "with args" << args.join(' ');
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
+ if(!APPLICATION->isFlatpak())
+ {
// FIXME: the pid here is fake. So if something depends on it, it will likely misbehave
return IndirectOpen([&]()
{
return QProcess::startDetached(application, args, workingDirectory);
}, pid);
+ }
+ else
+ {
+ return QProcess::startDetached(application, args, workingDirectory, pid);
+ }
#else
return QProcess::startDetached(application, args, workingDirectory, pid);
#endif
@@ -140,7 +203,14 @@ bool openUrl(const QUrl &url)
return QDesktopServices::openUrl(url);
};
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
- return IndirectOpen(f);
+ if(!APPLICATION->isFlatpak())
+ {
+ return IndirectOpen(f);
+ }
+ else
+ {
+ return f();
+ }
#else
return f();
#endif
diff --git a/launcher/java/JavaUtils.cpp b/launcher/java/JavaUtils.cpp
index 6e5dfeae..65a8b1db 100644
--- a/launcher/java/JavaUtils.cpp
+++ b/launcher/java/JavaUtils.cpp
@@ -153,7 +153,7 @@ QStringList addJavasFromEnv(QList<QString> javas)
{
QByteArray env = qgetenv("POLYMC_JAVA_PATHS");
#if defined(Q_OS_WIN32)
- QList<QString> javaPaths = QString::fromLocal8Bit(env).split(QLatin1String(";"));
+ QList<QString> javaPaths = QString::fromLocal8Bit(env).replace("\\", "/").split(QLatin1String(";"));
#else
QList<QString> javaPaths = QString::fromLocal8Bit(env).split(QLatin1String(":"));
#endif
@@ -355,7 +355,7 @@ QList<QString> JavaUtils::FindJavaPaths()
}
}
- return candidates;
+ return addJavasFromEnv(candidates);
}
#elif defined(Q_OS_MAC)
diff --git a/launcher/launch/LaunchTask.cpp b/launcher/launch/LaunchTask.cpp
index 231a6398..d5442a2b 100644
--- a/launcher/launch/LaunchTask.cpp
+++ b/launcher/launch/LaunchTask.cpp
@@ -1,18 +1,38 @@
-/* Copyright 2013-2021 MultiMC Contributors
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * PolyMC - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
- * Authors: Orochimarufan <orochimarufan.x3@gmail.com>
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
*
- * 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
+ * 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.
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*
- * 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.
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Authors: Orochimarufan <orochimarufan.x3@gmail.com>
+ *
+ * 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 "launch/LaunchTask.h"
@@ -212,7 +232,7 @@ shared_qobject_ptr<LogModel> LaunchTask::getLogModel()
m_logModel->setMaxLines(m_instance->getConsoleMaxLines());
m_logModel->setStopOnOverflow(m_instance->shouldStopOnConsoleOverflow());
// FIXME: should this really be here?
- m_logModel->setOverflowMessage(tr("PolyMC stopped watching the game log because the log length surpassed %1 lines.\n"
+ m_logModel->setOverflowMessage(tr("Stopped watching the game log because the log length surpassed %1 lines.\n"
"You may have to fix your mods because the game is still logging to files and"
" likely wasting harddrive space at an alarming rate!").arg(m_logModel->getMaxLines()));
}
diff --git a/launcher/launch/steps/CheckJava.cpp b/launcher/launch/steps/CheckJava.cpp
index d3f2148c..c2ebb334 100644
--- a/launcher/launch/steps/CheckJava.cpp
+++ b/launcher/launch/steps/CheckJava.cpp
@@ -1,16 +1,36 @@
-/* Copyright 2013-2021 MultiMC Contributors
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * PolyMC - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
- * 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
+ * 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.
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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.
*
- * 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.
+ * 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 "CheckJava.h"
@@ -87,14 +107,14 @@ void CheckJava::checkJavaFinished(JavaCheckResult result)
// Error message displayed if java can't start
emit logLine(QString("Could not start java:"), MessageLevel::Error);
emit logLines(result.errorLog.split('\n'), MessageLevel::Error);
- emit logLine("\nCheck your PolyMC Java settings.", MessageLevel::Launcher);
+ emit logLine(QString("\nCheck your Java settings."), MessageLevel::Launcher);
printSystemInfo(false, false);
emitFailed(QString("Could not start java!"));
return;
}
case JavaCheckResult::Validity::ReturnedInvalidData:
{
- emit logLine(QString("Java checker returned some invalid data PolyMC doesn't understand:"), MessageLevel::Error);
+ emit logLine(QString("Java checker returned some invalid data we don't understand:"), MessageLevel::Error);
emit logLines(result.outLog.split('\n'), MessageLevel::Warning);
emit logLine("\nMinecraft might not start properly.", MessageLevel::Launcher);
printSystemInfo(false, false);
diff --git a/launcher/launch/steps/QuitAfterGameStop.cpp b/launcher/launch/steps/QuitAfterGameStop.cpp
new file mode 100644
index 00000000..f9eced99
--- /dev/null
+++ b/launcher/launch/steps/QuitAfterGameStop.cpp
@@ -0,0 +1,26 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * PolyMC - Minecraft Launcher
+ * Copyright (C) 2022 dada513 <dada513@protonmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "QuitAfterGameStop.h"
+#include <launch/LaunchTask.h>
+#include "Application.h"
+
+void QuitAfterGameStop::executeTask()
+{
+ APPLICATION->quit();
+}
diff --git a/launcher/launch/steps/QuitAfterGameStop.h b/launcher/launch/steps/QuitAfterGameStop.h
new file mode 100644
index 00000000..1ce14da9
--- /dev/null
+++ b/launcher/launch/steps/QuitAfterGameStop.h
@@ -0,0 +1,35 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * PolyMC - Minecraft Launcher
+ * Copyright (C) 2022 dada513 <dada513@protonmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <launch/LaunchStep.h>
+
+class QuitAfterGameStop: public LaunchStep
+{
+ Q_OBJECT
+public:
+ explicit QuitAfterGameStop(LaunchTask *parent) :LaunchStep(parent){};
+ virtual ~QuitAfterGameStop() {};
+
+ virtual void executeTask();
+ virtual bool canAbort() const
+ {
+ return false;
+ }
+};
diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp
index 7c289300..fd933df7 100644
--- a/launcher/minecraft/MinecraftInstance.cpp
+++ b/launcher/minecraft/MinecraftInstance.cpp
@@ -34,6 +34,7 @@
*/
#include "MinecraftInstance.h"
+#include "BuildConfig.h"
#include "minecraft/launch/CreateGameFolders.h"
#include "minecraft/launch/ExtractNatives.h"
#include "minecraft/launch/PrintInstanceInfo.h"
@@ -55,6 +56,7 @@
#include "launch/steps/PreLaunchCommand.h"
#include "launch/steps/TextPrint.h"
#include "launch/steps/CheckJava.h"
+#include "launch/steps/QuitAfterGameStop.h"
#include "minecraft/launch/LauncherPartLaunch.h"
#include "minecraft/launch/DirectJavaLaunch.h"
@@ -470,7 +472,7 @@ QStringList MinecraftInstance::processMinecraftArgs(
}
// blatant self-promotion.
- token_mapping["profile_name"] = token_mapping["version_name"] = "PolyMC";
+ token_mapping["profile_name"] = token_mapping["version_name"] = BuildConfig.LAUNCHER_NAME;
token_mapping["version_type"] = profile->getMinecraftVersionType();
@@ -971,6 +973,11 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt
{
process->setCensorFilter(createCensorFilterFromSession(session));
}
+ if(APPLICATION->settings()->get("QuitAfterGameStop").toBool())
+ {
+ auto step = new QuitAfterGameStop(pptr);
+ process->appendStep(step);
+ }
m_launchProcess = process;
emit launchTaskChanged(m_launchProcess);
return m_launchProcess;
diff --git a/launcher/minecraft/World.cpp b/launcher/minecraft/World.cpp
index 2937c116..dc756e06 100644
--- a/launcher/minecraft/World.cpp
+++ b/launcher/minecraft/World.cpp
@@ -17,6 +17,7 @@
#include <QString>
#include <QDebug>
#include <QSaveFile>
+#include <QDirIterator>
#include "World.h"
#include "GZip.h"
@@ -187,6 +188,26 @@ bool putLevelDatDataToFS(const QFileInfo &file, QByteArray & data)
return f.commit();
}
+int64_t calculateWorldSize(const QFileInfo &file)
+{
+ if (file.isFile() && file.suffix() == "zip")
+ {
+ return file.size();
+ }
+ else if(file.isDir())
+ {
+ QDirIterator it(file.absoluteFilePath(), QDir::Files, QDirIterator::Subdirectories);
+ int64_t total = 0;
+ while (it.hasNext())
+ {
+ total += it.fileInfo().size();
+ it.next();
+ }
+ return total;
+ }
+ return -1;
+}
+
World::World(const QFileInfo &file)
{
repath(file);
@@ -196,6 +217,7 @@ void World::repath(const QFileInfo &file)
{
m_containerFile = file;
m_folderName = file.fileName();
+ m_size = calculateWorldSize(file);
if(file.isFile() && file.suffix() == "zip")
{
m_iconFile = QString();
@@ -482,6 +504,7 @@ void World::loadFromLevelDat(QByteArray data)
if(randomSeed) {
qDebug() << "Seed:" << *randomSeed;
}
+ qDebug() << "Size:" << m_size;
qDebug() << "GameType:" << m_gameType.toLogString();
}
diff --git a/launcher/minecraft/World.h b/launcher/minecraft/World.h
index 35e32788..0f587620 100644
--- a/launcher/minecraft/World.h
+++ b/launcher/minecraft/World.h
@@ -52,6 +52,10 @@ public:
{
return m_iconFile;
}
+ int64_t bytes() const
+ {
+ return m_size;
+ }
QDateTime lastPlayed() const
{
return m_lastPlayed;
@@ -105,6 +109,7 @@ protected:
QString m_iconFile;
QDateTime levelDatTime;
QDateTime m_lastPlayed;
+ int64_t m_size;
int64_t m_randomSeed = 0;
GameType m_gameType;
bool is_valid = false;
diff --git a/launcher/minecraft/WorldList.cpp b/launcher/minecraft/WorldList.cpp
index dcdbc321..344bea63 100644
--- a/launcher/minecraft/WorldList.cpp
+++ b/launcher/minecraft/WorldList.cpp
@@ -14,6 +14,8 @@
*/
#include "WorldList.h"
+
+#include "Application.h"
#include <FileSystem.h>
#include <QMimeData>
#include <QUrl>
@@ -150,7 +152,7 @@ bool WorldList::resetIcon(int row)
int WorldList::columnCount(const QModelIndex &parent) const
{
- return 3;
+ return 4;
}
QVariant WorldList::data(const QModelIndex &index, int role) const
@@ -164,6 +166,8 @@ QVariant WorldList::data(const QModelIndex &index, int role) const
if (row < 0 || row >= worlds.size())
return QVariant();
+ QLocale locale;
+
auto & world = worlds[row];
switch (role)
{
@@ -179,6 +183,9 @@ QVariant WorldList::data(const QModelIndex &index, int role) const
case LastPlayedColumn:
return world.lastPlayed();
+ case SizeColumn:
+ return locale.formattedDataSize(world.bytes());
+
default:
return QVariant();
}
@@ -207,6 +214,10 @@ QVariant WorldList::data(const QModelIndex &index, int role) const
{
return world.lastPlayed();
}
+ case SizeRole:
+ {
+ return locale.formattedDataSize(world.bytes());
+ }
case IconFileRole:
{
return world.iconFile();
@@ -229,6 +240,9 @@ QVariant WorldList::headerData(int section, Qt::Orientation orientation, int rol
return tr("Game Mode");
case LastPlayedColumn:
return tr("Last Played");
+ case SizeColumn:
+ //: World size on disk
+ return tr("Size");
default:
return QVariant();
}
@@ -242,6 +256,8 @@ QVariant WorldList::headerData(int section, Qt::Orientation orientation, int rol
return tr("Game mode of the world.");
case LastPlayedColumn:
return tr("Date and time the world was last played.");
+ case SizeColumn:
+ return tr("Size of the world on disk.");
default:
return QVariant();
}
diff --git a/launcher/minecraft/WorldList.h b/launcher/minecraft/WorldList.h
index 8e238ee3..5138e583 100644
--- a/launcher/minecraft/WorldList.h
+++ b/launcher/minecraft/WorldList.h
@@ -32,7 +32,8 @@ public:
{
NameColumn,
GameModeColumn,
- LastPlayedColumn
+ LastPlayedColumn,
+ SizeColumn
};
enum Roles
@@ -43,6 +44,7 @@ public:
NameRole,
GameModeRole,
LastPlayedRole,
+ SizeRole,
IconFileRole
};
diff --git a/launcher/minecraft/launch/LauncherPartLaunch.cpp b/launcher/minecraft/launch/LauncherPartLaunch.cpp
index d15d7e9d..173f29b5 100644
--- a/launcher/minecraft/launch/LauncherPartLaunch.cpp
+++ b/launcher/minecraft/launch/LauncherPartLaunch.cpp
@@ -170,6 +170,7 @@ void LauncherPartLaunch::on_state(LoggedProcess::State state)
{
if (APPLICATION->settings()->get("CloseAfterLaunch").toBool())
APPLICATION->showMainWindow();
+
m_parent->setPid(-1);
// if the exit code wasn't 0, report this as a crash
auto exitCode = m_process.exitCode();
diff --git a/launcher/modplatform/ModAPI.h b/launcher/modplatform/ModAPI.h
new file mode 100644
index 00000000..ae6ac80f
--- /dev/null
+++ b/launcher/modplatform/ModAPI.h
@@ -0,0 +1,38 @@
+#pragma once
+
+#include <QString>
+#include <QList>
+
+namespace ModPlatform {
+class ListModel;
+}
+
+class ModAPI {
+ protected:
+ using CallerType = ModPlatform::ListModel;
+
+ public:
+ virtual ~ModAPI() = default;
+
+ // https://docs.curseforge.com/?http#tocS_ModLoaderType
+ enum ModLoaderType { Any = 0, Forge = 1, Cauldron = 2, LiteLoader = 3, Fabric = 4 };
+
+ struct SearchArgs {
+ int offset;
+ QString search;
+ QString sorting;
+ ModLoaderType mod_loader;
+ QString version;
+ };
+
+ virtual void searchMods(CallerType* caller, SearchArgs&& args) const = 0;
+
+
+ struct VersionSearchArgs {
+ QString addonId;
+ QList<QString> mcVersions;
+ ModLoaderType loader;
+ };
+
+ virtual void getVersions(CallerType* caller, VersionSearchArgs&& args) const = 0;
+};
diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h
new file mode 100644
index 00000000..7e1cf254
--- /dev/null
+++ b/launcher/modplatform/ModIndex.h
@@ -0,0 +1,42 @@
+#pragma once
+
+#include <QList>
+#include <QMetaType>
+#include <QString>
+#include <QVariant>
+#include <QVector>
+
+namespace ModPlatform {
+
+struct ModpackAuthor {
+ QString name;
+ QString url;
+};
+
+struct IndexedVersion {
+ QVariant addonId;
+ QVariant fileId;
+ QString version;
+ QVector<QString> mcVersion;
+ QString downloadUrl;
+ QString date;
+ QString fileName;
+ QVector<QString> loaders = {};
+};
+
+struct IndexedPack {
+ QVariant addonId;
+ QString name;
+ QString description;
+ QList<ModpackAuthor> authors;
+ QString logoName;
+ QString logoUrl;
+ QString websiteUrl;
+
+ bool versionsLoaded = false;
+ QVector<IndexedVersion> versions;
+};
+
+} // namespace ModPlatform
+
+Q_DECLARE_METATYPE(ModPlatform::IndexedPack)
diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h
new file mode 100644
index 00000000..8654a693
--- /dev/null
+++ b/launcher/modplatform/flame/FlameAPI.h
@@ -0,0 +1,32 @@
+#pragma once
+
+#include "modplatform/helpers/NetworkModAPI.h"
+
+class FlameAPI : public NetworkModAPI {
+ private:
+ inline auto getModSearchURL(SearchArgs& args) const -> QString override
+ {
+ return QString(
+ "https://addons-ecs.forgesvc.net/api/v2/addon/search?"
+ "gameId=432&"
+ "categoryId=0&"
+ "sectionId=6&"
+
+ "index=%1&"
+ "pageSize=25&"
+ "searchFilter=%2&"
+ "sort=%3&"
+ "modLoaderType=%4&"
+ "gameVersion=%5")
+ .arg(args.offset)
+ .arg(args.search)
+ .arg(args.sorting)
+ .arg(args.mod_loader)
+ .arg(args.version);
+ };
+
+ inline auto getVersionsURL(VersionSearchArgs& args) const -> QString override
+ {
+ return QString("https://addons-ecs.forgesvc.net/api/v2/addon/%1/files").arg(args.addonId);
+ };
+};
diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp
index 4adaf5f1..2c3adee4 100644
--- a/launcher/modplatform/flame/FlameModIndex.cpp
+++ b/launcher/modplatform/flame/FlameModIndex.cpp
@@ -1,13 +1,11 @@
-#include <QObject>
#include "FlameModIndex.h"
+
#include "Json.h"
-#include "net/NetJob.h"
-#include "BaseInstance.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
+#include "net/NetJob.h"
-
-void FlameMod::loadIndexedPack(FlameMod::IndexedPack & pack, QJsonObject & obj)
+void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj)
{
pack.addonId = Json::requireInteger(obj, "id");
pack.name = Json::requireString(obj, "name");
@@ -16,10 +14,10 @@ void FlameMod::loadIndexedPack(FlameMod::IndexedPack & pack, QJsonObject & obj)
bool thumbnailFound = false;
auto attachments = Json::requireArray(obj, "attachments");
- for(auto attachmentRaw: attachments) {
+ for (auto attachmentRaw : attachments) {
auto attachmentObj = Json::requireObject(attachmentRaw);
bool isDefault = attachmentObj.value("isDefault").toBool(false);
- if(isDefault) {
+ if (isDefault) {
thumbnailFound = true;
pack.logoName = Json::requireString(attachmentObj, "title");
pack.logoUrl = Json::requireString(attachmentObj, "thumbnailUrl");
@@ -27,37 +25,35 @@ void FlameMod::loadIndexedPack(FlameMod::IndexedPack & pack, QJsonObject & obj)
}
}
- if(!thumbnailFound) {
- throw JSONValidationError(QString("Pack without an icon, skipping: %1").arg(pack.name));
- }
-
+ if (!thumbnailFound) { throw JSONValidationError(QString("Pack without an icon, skipping: %1").arg(pack.name)); }
auto authors = Json::requireArray(obj, "authors");
- for(auto authorIter: authors) {
+ for (auto authorIter : authors) {
auto author = Json::requireObject(authorIter);
- FlameMod::ModpackAuthor packAuthor;
+ ModPlatform::ModpackAuthor packAuthor;
packAuthor.name = Json::requireString(author, "name");
packAuthor.url = Json::requireString(author, "url");
pack.authors.append(packAuthor);
}
}
-void FlameMod::loadIndexedPackVersions(FlameMod::IndexedPack & pack, QJsonArray & arr, const shared_qobject_ptr<QNetworkAccessManager>& network, BaseInstance * inst)
+void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
+ QJsonArray& arr,
+ const shared_qobject_ptr<QNetworkAccessManager>& network,
+ BaseInstance* inst)
{
- QVector<FlameMod::IndexedVersion> unsortedVersions;
- bool hasFabric = !((MinecraftInstance *)inst)->getPackProfile()->getComponentVersion("net.fabricmc.fabric-loader").isEmpty();
- QString mcVersion = ((MinecraftInstance *)inst)->getPackProfile()->getComponentVersion("net.minecraft");
+ QVector<ModPlatform::IndexedVersion> unsortedVersions;
+ bool hasFabric = !(dynamic_cast<MinecraftInstance*>(inst))->getPackProfile()->getComponentVersion("net.fabricmc.fabric-loader").isEmpty();
+ QString mcVersion = (dynamic_cast<MinecraftInstance*>(inst))->getPackProfile()->getComponentVersion("net.minecraft");
- for(auto versionIter: arr) {
+ for (auto versionIter : arr) {
auto obj = versionIter.toObject();
auto versionArray = Json::requireArray(obj, "gameVersion");
- if (versionArray.isEmpty()) {
- continue;
- }
+ if (versionArray.isEmpty()) { continue; }
- FlameMod::IndexedVersion file;
- for(auto mcVer : versionArray){
+ ModPlatform::IndexedVersion file;
+ for (auto mcVer : versionArray) {
file.mcVersion.append(mcVer.toString());
}
@@ -70,29 +66,27 @@ void FlameMod::loadIndexedPackVersions(FlameMod::IndexedPack & pack, QJsonArray
auto modules = Json::requireArray(obj, "modules");
bool is_valid_fabric_version = false;
- for(auto m : modules){
- auto fname = Json::requireString(m.toObject(),"foldername");
+ for (auto m : modules) {
+ auto fname = Json::requireString(m.toObject(), "foldername");
// FIXME: This does not work properly when a mod supports more than one mod loader, since
// they bundle the meta files for all of them in the same arquive, even when that version
// doesn't support the given mod loader.
- if(hasFabric){
- if(fname == "fabric.mod.json"){
+ if (hasFabric) {
+ if (fname == "fabric.mod.json") {
is_valid_fabric_version = true;
break;
}
- }
- else break;
+ } else
+ break;
// NOTE: Since we're not validating forge versions, we can just skip this loop.
}
- if(hasFabric && !is_valid_fabric_version)
- continue;
+ if (hasFabric && !is_valid_fabric_version) continue;
unsortedVersions.append(file);
}
- auto orderSortPredicate = [](const IndexedVersion & a, const IndexedVersion & b) -> bool
- {
- //dates are in RFC 3339 format
+ auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool {
+ // dates are in RFC 3339 format
return a.date > b.date;
};
std::sort(unsortedVersions.begin(), unsortedVersions.end(), orderSortPredicate);
diff --git a/launcher/modplatform/flame/FlameModIndex.h b/launcher/modplatform/flame/FlameModIndex.h
index 0293bb23..d3171d94 100644
--- a/launcher/modplatform/flame/FlameModIndex.h
+++ b/launcher/modplatform/flame/FlameModIndex.h
@@ -3,48 +3,18 @@
//
#pragma once
-#include <QList>
-#include <QMetaType>
-#include <QString>
-#include <QVector>
-#include <QNetworkAccessManager>
-#include <QObjectPtr.h>
-#include "net/NetJob.h"
-#include "BaseInstance.h"
-
-namespace FlameMod {
- struct ModpackAuthor {
- QString name;
- QString url;
- };
- struct IndexedVersion {
- int addonId;
- int fileId;
- QString version;
- QVector<QString> mcVersion;
- QString downloadUrl;
- QString date;
- QString fileName;
- };
+#include "modplatform/ModIndex.h"
- struct IndexedPack
- {
- int addonId;
- QString name;
- QString description;
- QList<ModpackAuthor> authors;
- QString logoName;
- QString logoUrl;
- QString websiteUrl;
-
- bool versionsLoaded = false;
- QVector<IndexedVersion> versions;
- };
+#include "BaseInstance.h"
+#include <QNetworkAccessManager>
- void loadIndexedPack(IndexedPack & m, QJsonObject & obj);
- void loadIndexedPackVersions(IndexedPack &pack, QJsonArray &arr, const shared_qobject_ptr<QNetworkAccessManager> &network, BaseInstance *inst);
+namespace FlameMod {
-}
+void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj);
+void loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
+ QJsonArray& arr,
+ const shared_qobject_ptr<QNetworkAccessManager>& network,
+ BaseInstance* inst);
-Q_DECLARE_METATYPE(FlameMod::IndexedPack)
+} // namespace FlameMod
diff --git a/launcher/modplatform/helpers/NetworkModAPI.cpp b/launcher/modplatform/helpers/NetworkModAPI.cpp
new file mode 100644
index 00000000..6829b837
--- /dev/null
+++ b/launcher/modplatform/helpers/NetworkModAPI.cpp
@@ -0,0 +1,60 @@
+#include "NetworkModAPI.h"
+
+#include "ui/pages/modplatform/ModModel.h"
+
+#include "Application.h"
+#include "net/NetJob.h"
+
+void NetworkModAPI::searchMods(CallerType* caller, SearchArgs&& args) const
+{
+ auto netJob = new NetJob(QString("%1::Search").arg(caller->debugName()), APPLICATION->network());
+ auto searchUrl = getModSearchURL(args);
+
+ auto response = new QByteArray();
+ netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), response));
+
+ QObject::connect(netJob, &NetJob::started, caller, [caller, netJob] { caller->setActiveJob(netJob); });
+ QObject::connect(netJob, &NetJob::failed, caller, &CallerType::searchRequestFailed);
+ QObject::connect(netJob, &NetJob::succeeded, caller, [caller, response] {
+ QJsonParseError parse_error{};
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
+ if (parse_error.error != QJsonParseError::NoError) {
+ qWarning() << "Error while parsing JSON response from " << caller->debugName() << " at " << parse_error.offset
+ << " reason: " << parse_error.errorString();
+ qWarning() << *response;
+ return;
+ }
+
+ caller->searchRequestFinished(doc);
+ });
+
+ netJob->start();
+}
+
+void NetworkModAPI::getVersions(CallerType* caller, VersionSearchArgs&& args) const
+{
+ auto netJob = new NetJob(QString("%1::ModVersions(%2)").arg(caller->debugName()).arg(args.addonId), APPLICATION->network());
+ auto response = new QByteArray();
+
+ netJob->addNetAction(Net::Download::makeByteArray(getVersionsURL(args), response));
+
+ QObject::connect(netJob, &NetJob::succeeded, caller, [response, caller, args] {
+ QJsonParseError parse_error{};
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
+ if (parse_error.error != QJsonParseError::NoError) {
+ qWarning() << "Error while parsing JSON response from " << caller->debugName() << " at " << parse_error.offset
+ << " reason: " << parse_error.errorString();
+ qWarning() << *response;
+ return;
+ }
+
+ caller->versionRequestSucceeded(doc, args.addonId);
+ });
+
+ QObject::connect(netJob, &NetJob::finished, caller, [response, netJob] {
+ netJob->deleteLater();
+ delete response;
+ });
+
+ netJob->start();
+}
diff --git a/launcher/modplatform/helpers/NetworkModAPI.h b/launcher/modplatform/helpers/NetworkModAPI.h
new file mode 100644
index 00000000..000620b2
--- /dev/null
+++ b/launcher/modplatform/helpers/NetworkModAPI.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include "modplatform/ModAPI.h"
+
+class NetworkModAPI : public ModAPI {
+ public:
+ void searchMods(CallerType* caller, SearchArgs&& args) const override;
+ void getVersions(CallerType* caller, VersionSearchArgs&& args) const override;
+
+ protected:
+ virtual auto getModSearchURL(SearchArgs& args) const -> QString = 0;
+ virtual auto getVersionsURL(VersionSearchArgs& args) const -> QString = 0;
+};
diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h
new file mode 100644
index 00000000..30952e99
--- /dev/null
+++ b/launcher/modplatform/modrinth/ModrinthAPI.h
@@ -0,0 +1,73 @@
+#pragma once
+
+#include "modplatform/helpers/NetworkModAPI.h"
+
+#include <QDebug>
+
+class ModrinthAPI : public NetworkModAPI {
+ public:
+ inline auto getAuthorURL(const QString& name) const -> QString { return "https://modrinth.com/user/" + name; };
+
+ private:
+ inline auto getModSearchURL(SearchArgs& args) const -> QString override
+ {
+ if (!validateModLoader(args.mod_loader)) {
+ qWarning() << "Modrinth only have Forge and Fabric-compatible mods!";
+ return "";
+ }
+
+ return QString(
+ "https://api.modrinth.com/v2/search?"
+ "offset=%1&"
+ "limit=25&"
+ "query=%2&"
+ "index=%3&"
+ "facets=[[\"categories:%4\"],[\"versions:%5\"],[\"project_type:mod\"]]")
+ .arg(args.offset)
+ .arg(args.search)
+ .arg(args.sorting)
+ .arg(getModLoaderString(args.mod_loader))
+ .arg(args.version);
+ };
+
+ inline auto getVersionsURL(VersionSearchArgs& args) const -> QString override
+ {
+ return QString("https://api.modrinth.com/v2/project/%1/version?"
+ "game_versions=[%2]"
+ "loaders=[%3]")
+ .arg(args.addonId)
+ .arg(getGameVersionsString(args.mcVersions))
+ .arg(getModLoaderString(args.loader));
+ };
+
+ inline auto getGameVersionsString(QList<QString> mcVersions) const -> QString
+ {
+ QString s;
+ for(int i = 0; i < mcVersions.count(); i++){
+ s += mcVersions.at(i);
+ if(i < mcVersions.count() - 1)
+ s += ",";
+ }
+ return s;
+ }
+
+ inline auto getModLoaderString(ModLoaderType modLoader) const -> QString
+ {
+ switch (modLoader) {
+ case Any:
+ return "fabric, forge";
+ case Forge:
+ return "forge";
+ case Fabric:
+ return "fabric";
+ default:
+ return "";
+ }
+ }
+
+ inline auto validateModLoader(ModLoaderType modLoader) const -> bool
+ {
+ return modLoader == Any || modLoader == Forge || modLoader == Fabric;
+ }
+
+};
diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp
index 9581ca04..5b75f034 100644
--- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp
+++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp
@@ -1,50 +1,50 @@
-#include <QObject>
#include "ModrinthPackIndex.h"
+#include "ModrinthAPI.h"
#include "Json.h"
-#include "net/NetJob.h"
-#include "BaseInstance.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
+#include "net/NetJob.h"
+static ModrinthAPI api;
-void Modrinth::loadIndexedPack(Modrinth::IndexedPack & pack, QJsonObject & obj)
+void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj)
{
pack.addonId = Json::requireString(obj, "project_id");
pack.name = Json::requireString(obj, "title");
- pack.websiteUrl = Json::ensureString(obj, "page_url", "");
+ pack.websiteUrl = "https://modrinth.com/mod/" + Json::ensureString(obj, "slug", "");
pack.description = Json::ensureString(obj, "description", "");
pack.logoUrl = Json::requireString(obj, "icon_url");
- pack.logoName = pack.addonId;
+ pack.logoName = pack.addonId.toString();
- Modrinth::ModpackAuthor modAuthor;
+ ModPlatform::ModpackAuthor modAuthor;
modAuthor.name = Json::requireString(obj, "author");
- modAuthor.url = "https://modrinth.com/user/"+modAuthor.name;
- pack.author = modAuthor;
+ modAuthor.url = api.getAuthorURL(modAuthor.name);
+ pack.authors.append(modAuthor);
}
-void Modrinth::loadIndexedPackVersions(Modrinth::IndexedPack & pack, QJsonArray & arr, const shared_qobject_ptr<QNetworkAccessManager>& network, BaseInstance * inst)
+void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
+ QJsonArray& arr,
+ const shared_qobject_ptr<QNetworkAccessManager>& network,
+ BaseInstance* inst)
{
- QVector<Modrinth::IndexedVersion> unsortedVersions;
- bool hasFabric = !((MinecraftInstance *)inst)->getPackProfile()->getComponentVersion("net.fabricmc.fabric-loader").isEmpty();
- QString mcVersion = ((MinecraftInstance *)inst)->getPackProfile()->getComponentVersion("net.minecraft");
+ QVector<ModPlatform::IndexedVersion> unsortedVersions;
+ QString mcVersion = (static_cast<MinecraftInstance*>(inst))->getPackProfile()->getComponentVersion("net.minecraft");
- for(auto versionIter: arr) {
+ for (auto versionIter : arr) {
auto obj = versionIter.toObject();
- Modrinth::IndexedVersion file;
- file.addonId = Json::requireString(obj,"project_id") ;
+ ModPlatform::IndexedVersion file;
+ file.addonId = Json::requireString(obj, "project_id");
file.fileId = Json::requireString(obj, "id");
file.date = Json::requireString(obj, "date_published");
auto versionArray = Json::requireArray(obj, "game_versions");
- if (versionArray.empty()) {
- continue;
- }
- for(auto mcVer : versionArray){
+ if (versionArray.empty()) { continue; }
+ for (auto mcVer : versionArray) {
file.mcVersion.append(mcVer.toString());
}
- auto loaders = Json::requireArray(obj,"loaders");
- for(auto loader : loaders){
+ auto loaders = Json::requireArray(obj, "loaders");
+ for (auto loader : loaders) {
file.loaders.append(loader.toString());
}
file.version = Json::requireString(obj, "name");
@@ -55,21 +55,11 @@ void Modrinth::loadIndexedPackVersions(Modrinth::IndexedPack & pack, QJsonArray
// Find correct file (needed in cases where one version may have multiple files)
// Will default to the last one if there's no primary (though I think Modrinth requires that
// at least one file is primary, idk)
- while (i < files.count()){
+ // NOTE: files.count() is 1-indexed, so we need to subtract 1 to become 0-indexed
+ while (i < files.count() - 1){
auto parent = files[i].toObject();
auto fileName = Json::requireString(parent, "filename");
- // Grab the correct mod loader
- if(hasFabric){
- if(fileName.contains("forge",Qt::CaseInsensitive)){
- i++;
- continue;
- }
- } else if(fileName.contains("fabric", Qt::CaseInsensitive)){
- i++;
- continue;
- }
-
// Grab the primary file, if available
if(Json::requireBoolean(parent, "primary"))
break;
@@ -78,16 +68,15 @@ void Modrinth::loadIndexedPackVersions(Modrinth::IndexedPack & pack, QJsonArray
}
auto parent = files[i].toObject();
- if(parent.contains("url")) {
+ if (parent.contains("url")) {
file.downloadUrl = Json::requireString(parent, "url");
file.fileName = Json::requireString(parent, "filename");
unsortedVersions.append(file);
}
}
- auto orderSortPredicate = [](const IndexedVersion & a, const IndexedVersion & b) -> bool
- {
- //dates are in RFC 3339 format
+ auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool {
+ // dates are in RFC 3339 format
return a.date > b.date;
};
std::sort(unsortedVersions.begin(), unsortedVersions.end(), orderSortPredicate);
diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.h b/launcher/modplatform/modrinth/ModrinthPackIndex.h
index 3a4cd270..fd17847a 100644
--- a/launcher/modplatform/modrinth/ModrinthPackIndex.h
+++ b/launcher/modplatform/modrinth/ModrinthPackIndex.h
@@ -1,48 +1,16 @@
#pragma once
-#include <QList>
-#include <QMetaType>
-#include <QString>
-#include <QVector>
-#include <QNetworkAccessManager>
-#include <QObjectPtr.h>
-#include "net/NetJob.h"
+#include "modplatform/ModIndex.h"
+
#include "BaseInstance.h"
+#include <QNetworkAccessManager>
namespace Modrinth {
-struct ModpackAuthor {
- QString name;
- QString url;
-};
-
-struct IndexedVersion {
- QString addonId;
- QString fileId;
- QString version;
- QVector<QString> mcVersion;
- QString downloadUrl;
- QString date;
- QString fileName;
- QVector<QString> loaders;
-};
-
-struct IndexedPack
-{
- QString addonId;
- QString name;
- QString description;
- ModpackAuthor author;
- QString logoName;
- QString logoUrl;
- QString websiteUrl;
-
- bool versionsLoaded = false;
- QVector<IndexedVersion> versions;
-};
-
-void loadIndexedPack(IndexedPack & m, QJsonObject & obj);
-void loadIndexedPackVersions(IndexedPack &pack, QJsonArray &arr, const shared_qobject_ptr<QNetworkAccessManager> &network, BaseInstance *inst);
-}
+void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj);
+void loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
+ QJsonArray& arr,
+ const shared_qobject_ptr<QNetworkAccessManager>& network,
+ BaseInstance* inst);
-Q_DECLARE_METATYPE(Modrinth::IndexedPack)
+} // namespace Modrinth
diff --git a/launcher/resources/pe_light/scalable/launcher.svg b/launcher/resources/pe_light/scalable/launcher.svg
index c192d503..a9dfe87a 100644
--- a/launcher/resources/pe_light/scalable/launcher.svg
+++ b/launcher/resources/pe_light/scalable/launcher.svg
@@ -3,18 +3,18 @@
<svg width="64" height="64" version="1.1" viewBox="0 0 16.933 16.933" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<linearGradient id="linearGradient84726" x1="4.4979" x2="12.435" y1="3.8011" y2="9.5681" gradientUnits="userSpaceOnUse">
- <stop stop-color="#88b858" offset="0"/>
- <stop stop-color="#72b147" offset=".5"/>
- <stop stop-color="#5a9a30" offset="1"/>
+ <stop stop-color="#dedede" offset="0"/>
+ <stop stop-color="#d2d2d2" offset=".5"/>
+ <stop stop-color="#c0c0c0" offset="1"/>
</linearGradient>
</defs>
<g>
- <path d="m3.561 16.016s0-3.5642 4.9056-3.5642c4.9069 0 4.9056 3.5642 4.9056 3.5642z" fill="#765338"/>
- <path d="m8.4667 12.452-4.9056 3.5642-3.0319-9.3311z" fill="#b7835a"/>
- <path d="m8.4667 12.452 7.9375-5.7669-3.0319 9.3311z" fill="#5b422d"/>
- <path d="m8.8308 12.716-0.36417 0.26458-0.36417-0.26458c0-0.26458 0.36417-0.26458 0.36417-0.26458s0.36417 0 0.36417 0.26458z" fill="#72b147"/>
- <path d="m8.4667 12.452s-2e-7 -5.7669 7.9375-5.7669l-0.22507 0.69269-0.91853 1.1965-0.91853 0.13819-0.91853 1.1965-0.91853 0.13819-0.91853 1.1965-0.91853 0.13819-0.91853 1.1965-0.91853 0.13819z" fill="#5a9a30"/>
- <path d="m8.1025 12.716-0.91853-0.13819-0.91853-1.1965-0.91853-0.13819-0.91853-1.1965-0.91853-0.13819-0.91853-1.1965-0.91853-0.13819-0.91853-1.1965-0.22507-0.69269c7.9375 1e-7 7.9375 5.7669 7.9375 5.7669z" fill="#88b858"/>
+ <path d="m3.561 16.016s0-3.5642 4.9056-3.5642c4.9069 0 4.9056 3.5642 4.9056 3.5642z" fill="#8f8f8f"/>
+ <path d="m8.4667 12.452-4.9056 3.5642-3.0319-9.3311z" fill="#c2c2c2"/>
+ <path d="m8.4667 12.452 7.9375-5.7669-3.0319 9.3311z" fill="#7c7c7c"/>
+ <path d="m8.8308 12.716-0.36417 0.26458-0.36417-0.26458c0-0.26458 0.36417-0.26458 0.36417-0.26458s0.36417 0 0.36417 0.26458z" fill="#d3d3d3"/>
+ <path d="m8.4667 12.452s-2e-7 -5.7669 7.9375-5.7669l-0.22507 0.69269-0.91853 1.1965-0.91853 0.13819-0.91853 1.1965-0.91853 0.13819-0.91853 1.1965-0.91853 0.13819-0.91853 1.1965-0.91853 0.13819z" fill="#bcbcbc"/>
+ <path d="m8.1025 12.716-0.91853-0.13819-0.91853-1.1965-0.91853-0.13819-0.91853-1.1965-0.91853-0.13819-0.91853-1.1965-0.91853-0.13819-0.91853-1.1965-0.22507-0.69269c7.9375 1e-7 7.9375 5.7669 7.9375 5.7669z" fill="#dedede"/>
<path d="m0.52917 6.6846 7.9375 5.7669 7.9375-5.7669-7.9375-5.7669z" fill="url(#linearGradient84726)"/>
</g>
<path d="m0.75424 7.3773-0.22507-0.69269 7.9375 5.7669 7.9375-5.7669-0.22507 0.69269-7.7124 5.6034z" fill-opacity="0"/>
diff --git a/launcher/ui/dialogs/AboutDialog.cpp b/launcher/ui/dialogs/AboutDialog.cpp
index ef96cc23..8dadb755 100644
--- a/launcher/ui/dialogs/AboutDialog.cpp
+++ b/launcher/ui/dialogs/AboutDialog.cpp
@@ -1,29 +1,63 @@
-/* Copyright 2013-2021 MultiMC Contributors
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * PolyMC - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
- * 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
+ * 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.
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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.
*
- * 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.
+ * 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 "AboutDialog.h"
+#include "BuildConfig.h"
#include "ui_AboutDialog.h"
#include <QIcon>
#include "Application.h"
#include "BuildConfig.h"
#include <net/NetJob.h>
+#include <qobject.h>
#include "HoeDown.h"
namespace {
+QString getLink(QString link, QString name) {
+ return QString("&lt;<a href='%1'>%2</a>&gt;").arg(link).arg(name);
+}
+
+QString getWebsite(QString link) {
+ return getLink(link, QObject::tr("Website"));
+}
+
+QString getGitHub(QString username) {
+ return getLink("https://github.com/" + username, "GitHub");
+}
+
// Credits
// This is a hack, but I can't think of a better way to do this easily without screwing with QTextDocument...
QString getCreditsHtml()
@@ -33,15 +67,29 @@ QString getCreditsHtml()
stream.setCodec(QTextCodec::codecForName("UTF-8"));
stream << "<center>\n";
- stream << "<h3>" << QObject::tr("PolyMC Developers", "About Credits") << "</h3>\n";
- stream << "<p>swirl &lt;<a href='mailto:swurl@swurl.xyz'>swurl@swurl.xyz </a>&gt;</p>\n";
- stream << "<p>LennyMcLennington &lt;<a href='mailto:lenny@sneed.church'>lenny@sneed.church</a>&gt;</p>\n";
+ //: %1 is the name of the launcher, determined at build time, e.g. "PolyMC Developers"
+ stream << "<h3>" << QObject::tr("%1 Developers", "About Credits").arg(BuildConfig.LAUNCHER_NAME) << "</h3>\n";
+ stream << QString("<p>LennyMcLennington %1</p>\n") .arg(getGitHub("LennyMcLennington"));
+ stream << QString("<p>Sefa Eyeoglu (Scrumplex) %1</p>\n") .arg(getWebsite("https://scrumplex.net"));
+ stream << QString("<p>dada513 %1</p>\n") .arg(getGitHub("dada513"));
+ stream << QString("<p>txtsd %1</p>\n") .arg(getGitHub("txtsd"));
+ stream << QString("<p>timoreo %1</p>\n") .arg(getGitHub("timoreo22"));
+ stream << QString("<p>Ezekiel Smith (ZekeSmith) %1</p>\n") .arg(getGitHub("ZekeSmith"));
+ stream << QString("<p>cozyGalvinism %1</p>\n") .arg(getGitHub("cozyGalvinism"));
+ stream << "<br />\n";
+
+ //: %1 is the name of the launcher, determined at build time, e.g. "PolyMC Contributors"
+ stream << "<h3>" << QObject::tr("%1 Contributors", "About Credits").arg(BuildConfig.LAUNCHER_NAME) << "</h3>\n";
+ stream << QString("<p>DioEgizio %1</p>\n") .arg(getGitHub("DioEgizio"));
+ stream << QString("<p>flowln %1</p>\n") .arg(getGitHub("flowln"));
+ stream << QString("<p>swirl %1</p>\n") .arg(getWebsite("https://swurl.xyz/"));
stream << "<br />\n";
// TODO: possibly retrieve from git history at build time?
- stream << "<h3>" << QObject::tr("MultiMC Developers", "About Credits") << "</h3>\n";
+ //: %1 is the name of the launcher, determined at build time, e.g. "PolyMC Developers"
+ stream << "<h3>" << QObject::tr("%1 Developers", "About Credits").arg("MultiMC") << "</h3>\n";
stream << "<p>Andrew Okin &lt;<a href='mailto:forkk@forkk.net'>forkk@forkk.net</a>&gt;</p>\n";
- stream << "<p>Petr Mrázek &lt;<a href='mailto:peterix@gmail.com'>peterix@gmail.com</a>&gt;</p>\n";
+ stream << QString("<p>Petr Mrázek &lt;<a href='mailto:peterix@gmail.com'>peterix@gmail.com</a>&gt;</p>\n");
stream << "<p>Sky Welch &lt;<a href='mailto:multimc@bunnies.io'>multimc@bunnies.io</a>&gt;</p>\n";
stream << "<p>Jan (02JanDal) &lt;<a href='mailto:02jandal@gmail.com'>02jandal@gmail.com</a>&gt;</p>\n";
stream << "<p>RoboSky &lt;<a href='https://twitter.com/RoboSky_'>@RoboSky_</a>&gt;</p>\n";
diff --git a/launcher/ui/dialogs/AboutDialog.ui b/launcher/ui/dialogs/AboutDialog.ui
index 58275c66..f9665c30 100644
--- a/launcher/ui/dialogs/AboutDialog.ui
+++ b/launcher/ui/dialogs/AboutDialog.ui
@@ -87,7 +87,7 @@
</property>
</widget>
</item>
- <item>
+ <item>
<widget class="QLabel" name="versionLabel">
<property name="alignment">
<set>Qt::AlignCenter</set>
@@ -209,13 +209,10 @@
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
- <widget class="QTextEdit" name="creditsText">
- <property name="readOnly">
+ <widget class="QTextBrowser" name="creditsText">
+ <property name="openExternalLinks">
<bool>true</bool>
</property>
- <property name="textInteractionFlags">
- <set>Qt::TextBrowserInteraction</set>
- </property>
</widget>
</item>
</layout>
diff --git a/launcher/ui/pages/global/AccountListPage.cpp b/launcher/ui/pages/global/AccountListPage.cpp
index 1edba499..6e1e2183 100644
--- a/launcher/ui/pages/global/AccountListPage.cpp
+++ b/launcher/ui/pages/global/AccountListPage.cpp
@@ -161,10 +161,11 @@ void AccountListPage::on_actionAddMicrosoft_triggered()
CustomMessageBox::selectable(
this,
tr("Microsoft Accounts not available"),
+ //: %1 refers to the launcher itself
tr(
- "Microsoft accounts are only usable on macOS 10.13 or newer, with fully updated PolyMC.\n\n"
- "Please update both your operating system and PolyMC."
- ),
+ "Microsoft accounts are only usable on macOS 10.13 or newer, with fully updated %1.\n\n"
+ "Please update both your operating system and %1."
+ ).arg(BuildConfig.LAUNCHER_NAME),
QMessageBox::Warning
)->exec();
return;
diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp
index 6f7e1cc7..42ad5ae3 100644
--- a/launcher/ui/pages/global/LauncherPage.cpp
+++ b/launcher/ui/pages/global/LauncherPage.cpp
@@ -2,6 +2,7 @@
/*
* PolyMC - Minecraft Launcher
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
+ * Copyright (c) 2022 dada513 <dada513@protonmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -134,13 +135,31 @@ void LauncherPage::on_instDirBrowseBtn_clicked()
warning.setInformativeText(
tr("Do you really want to use this path? "
"Selecting \"No\" will close this and not alter your instance path."));
- warning.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
+ warning.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel);
int result = warning.exec();
- if (result == QMessageBox::Yes)
+ if (result == QMessageBox::Ok)
{
ui->instDirTextBox->setText(cooked_dir);
}
}
+ else if(APPLICATION->isFlatpak() && raw_dir.startsWith("/run/user"))
+ {
+ QMessageBox warning;
+ warning.setText(tr("You're trying to specify an instance folder "
+ "which was granted temporaily via Flatpak.\n"
+ "This is known to cause problems. "
+ "After a restart the launcher might break, "
+ "because it will no longer have access to that directory.\n\n"
+ "Granting PolyMC access to it via Flatseal is recommended."));
+ warning.setInformativeText(
+ tr("Do you want to proceed anyway?"));
+ warning.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel);
+ int result = warning.exec();
+ if (result == QMessageBox::Ok)
+ {
+ ui->instDirTextBox->setText(cooked_dir);
+ }
+ }
else
{
ui->instDirTextBox->setText(cooked_dir);
diff --git a/launcher/ui/pages/global/MinecraftPage.cpp b/launcher/ui/pages/global/MinecraftPage.cpp
index 9abae425..f49f5a92 100644
--- a/launcher/ui/pages/global/MinecraftPage.cpp
+++ b/launcher/ui/pages/global/MinecraftPage.cpp
@@ -94,6 +94,7 @@ void MinecraftPage::applySettings()
// Miscellaneous
s->set("CloseAfterLaunch", ui->closeAfterLaunchCheck->isChecked());
+ s->set("QuitAfterGameStop", ui->quitAfterGameStopCheck->isChecked());
}
void MinecraftPage::loadSettings()
@@ -113,6 +114,7 @@ void MinecraftPage::loadSettings()
ui->recordGameTime->setChecked(s->get("RecordGameTime").toBool());
ui->closeAfterLaunchCheck->setChecked(s->get("CloseAfterLaunch").toBool());
+ ui->quitAfterGameStopCheck->setChecked(s->get("QuitAfterGameStop").toBool());
}
void MinecraftPage::retranslate()
diff --git a/launcher/ui/pages/global/MinecraftPage.ui b/launcher/ui/pages/global/MinecraftPage.ui
index a28b1f59..decc9b8b 100644
--- a/launcher/ui/pages/global/MinecraftPage.ui
+++ b/launcher/ui/pages/global/MinecraftPage.ui
@@ -180,6 +180,16 @@
</property>
</widget>
</item>
+ <item>
+ <widget class="QCheckBox" name="quitAfterGameStopCheck">
+ <property name="toolTip">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;PolyMC will automatically exit if the game crashes or exists.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="text">
+ <string>Quit PolyMC after game window stops</string>
+ </property>
+ </widget>
+ </item>
</layout>
</widget>
</item>
diff --git a/launcher/ui/pages/instance/VersionPage.cpp b/launcher/ui/pages/instance/VersionPage.cpp
index 97c6fe8f..ed37dd1a 100644
--- a/launcher/ui/pages/instance/VersionPage.cpp
+++ b/launcher/ui/pages/instance/VersionPage.cpp
@@ -2,6 +2,7 @@
/*
* PolyMC - Minecraft Launcher
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
+ * Copyright (c) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* 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
@@ -422,7 +423,7 @@ void VersionPage::on_actionDownload_All_triggered()
{
CustomMessageBox::selectable(
this, tr("Error"),
- tr("PolyMC cannot download Minecraft or update instances unless you have at least "
+ tr("Cannot download Minecraft or update instances unless you have at least "
"one account added.\nPlease add your Mojang or Minecraft account."),
QMessageBox::Warning)->show();
return;
diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp
new file mode 100644
index 00000000..01b5d247
--- /dev/null
+++ b/launcher/ui/pages/modplatform/ModModel.cpp
@@ -0,0 +1,234 @@
+#include "ModModel.h"
+
+#include "BuildConfig.h"
+#include "Json.h"
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/PackProfile.h"
+#include "ui/dialogs/ModDownloadDialog.h"
+
+#include <QMessageBox>
+
+namespace ModPlatform {
+
+ListModel::ListModel(ModPage* parent) : QAbstractListModel(parent), m_parent(parent) {}
+
+auto ListModel::debugName() const -> QString
+{
+ return m_parent->debugName();
+}
+
+/******** Make data requests ********/
+
+void ListModel::fetchMore(const QModelIndex& parent)
+{
+ if (parent.isValid()) return;
+ if (nextSearchOffset == 0) {
+ qWarning() << "fetchMore with 0 offset is wrong...";
+ return;
+ }
+ performPaginatedSearch();
+}
+
+auto ListModel::data(const QModelIndex& index, int role) const -> QVariant
+{
+ int pos = index.row();
+ if (pos >= modpacks.size() || pos < 0 || !index.isValid()) { return QString("INVALID INDEX %1").arg(pos); }
+
+ ModPlatform::IndexedPack pack = modpacks.at(pos);
+ if (role == Qt::DisplayRole) {
+ return pack.name;
+ } else if (role == Qt::ToolTipRole) {
+ if (pack.description.length() > 100) {
+ // some magic to prevent to long tooltips and replace html linebreaks
+ QString edit = pack.description.left(97);
+ edit = edit.left(edit.lastIndexOf("<br>")).left(edit.lastIndexOf(" ")).append("...");
+ return edit;
+ }
+ return pack.description;
+ } else if (role == Qt::DecorationRole) {
+ if (m_logoMap.contains(pack.logoName)) { return (m_logoMap.value(pack.logoName)); }
+ QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder");
+ ((ListModel*)this)->requestLogo(pack.logoName, pack.logoUrl);
+ return icon;
+ } else if (role == Qt::UserRole) {
+ QVariant v;
+ v.setValue(pack);
+ return v;
+ }
+
+ return {};
+}
+
+void ListModel::requestModVersions(ModPlatform::IndexedPack const& current)
+{
+ m_parent->apiProvider()->getVersions(this,
+ { current.addonId.toString(), getMineVersions(), hasFabric() ? ModAPI::ModLoaderType::Fabric : ModAPI::ModLoaderType::Forge });
+}
+
+void ListModel::performPaginatedSearch()
+{
+ m_parent->apiProvider()->searchMods(this,
+ { nextSearchOffset, currentSearchTerm, getSorts()[currentSort], hasFabric() ? ModAPI::Fabric : ModAPI::Forge, getMineVersions().at(0) });
+}
+
+void ListModel::searchWithTerm(const QString& term, const int sort)
+{
+ if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort) { return; }
+ currentSearchTerm = term;
+ currentSort = sort;
+ if (jobPtr) {
+ jobPtr->abort();
+ searchState = ResetRequested;
+ return;
+ } else {
+ beginResetModel();
+ modpacks.clear();
+ endResetModel();
+ searchState = None;
+ }
+ nextSearchOffset = 0;
+ performPaginatedSearch();
+}
+
+void ListModel::getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback)
+{
+ if (m_logoMap.contains(logo)) {
+ callback(APPLICATION->metacache()
+ ->resolveEntry(m_parent->metaEntryBase(), QString("logos/%1").arg(logo.section(".", 0, 0)))
+ ->getFullPath());
+ } else {
+ requestLogo(logo, logoUrl);
+ }
+}
+
+void ListModel::requestLogo(QString logo, QString url)
+{
+ if (m_loadingLogos.contains(logo) || m_failedLogos.contains(logo)) { return; }
+
+ MetaEntryPtr entry =
+ APPLICATION->metacache()->resolveEntry(m_parent->metaEntryBase(), QString("logos/%1").arg(logo.section(".", 0, 0)));
+ auto job = new NetJob(QString("%1 Icon Download %2").arg(m_parent->debugName()).arg(logo), APPLICATION->network());
+ job->addNetAction(Net::Download::makeCached(QUrl(url), entry));
+
+ auto fullPath = entry->getFullPath();
+ QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] {
+ job->deleteLater();
+ emit logoLoaded(logo, QIcon(fullPath));
+ if (waitingCallbacks.contains(logo)) { waitingCallbacks.value(logo)(fullPath); }
+ });
+
+ QObject::connect(job, &NetJob::failed, this, [this, logo, job] {
+ job->deleteLater();
+ emit logoFailed(logo);
+ });
+
+ job->start();
+ m_loadingLogos.append(logo);
+}
+
+/******** Request callbacks ********/
+
+void ListModel::logoLoaded(QString logo, QIcon out)
+{
+ m_loadingLogos.removeAll(logo);
+ m_logoMap.insert(logo, out);
+ for (int i = 0; i < modpacks.size(); i++) {
+ if (modpacks[i].logoName == logo) { emit dataChanged(createIndex(i, 0), createIndex(i, 0), { Qt::DecorationRole }); }
+ }
+}
+
+void ListModel::logoFailed(QString logo)
+{
+ m_failedLogos.append(logo);
+ m_loadingLogos.removeAll(logo);
+}
+
+void ListModel::searchRequestFinished(QJsonDocument& doc)
+{
+ jobPtr.reset();
+
+ QList<ModPlatform::IndexedPack> newList;
+ auto packs = documentToArray(doc);
+
+ for (auto packRaw : packs) {
+ auto packObj = packRaw.toObject();
+
+ ModPlatform::IndexedPack pack;
+ try {
+ loadIndexedPack(pack, packObj);
+ newList.append(pack);
+ } catch (const JSONValidationError& e) {
+ qWarning() << "Error while loading mod from " << m_parent->debugName() << ": " << e.cause();
+ continue;
+ }
+ }
+
+ if (packs.size() < 25) {
+ searchState = Finished;
+ } else {
+ nextSearchOffset += 25;
+ searchState = CanPossiblyFetchMore;
+ }
+
+ beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1);
+ modpacks.append(newList);
+ endInsertRows();
+}
+
+void ListModel::searchRequestFailed(QString reason)
+{
+ if (jobPtr->first()->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 409) {
+ // 409 Gone, notify user to update
+ QMessageBox::critical(nullptr, tr("Error"),
+ //: %1 refers to the launcher itself
+ QString("%1 %2").arg(m_parent->displayName()).arg(tr("API version too old!\nPlease update %1!").arg(BuildConfig.LAUNCHER_NAME)));
+ // self-destruct
+ (dynamic_cast<ModDownloadDialog*>((dynamic_cast<ModPage*>(parent()))->parentWidget()))->reject();
+ }
+ jobPtr.reset();
+
+ if (searchState == ResetRequested) {
+ beginResetModel();
+ modpacks.clear();
+ endResetModel();
+
+ nextSearchOffset = 0;
+ performPaginatedSearch();
+ } else {
+ searchState = Finished;
+ }
+}
+
+void ListModel::versionRequestSucceeded(QJsonDocument doc, QString addonId)
+{
+ auto& current = m_parent->getCurrent();
+ if (addonId != current.addonId) { return; }
+
+ QJsonArray arr = doc.array();
+ try {
+ loadIndexedPackVersions(current, arr);
+ } catch (const JSONValidationError& e) {
+ qDebug() << doc;
+ qWarning() << "Error while reading " << debugName() << " mod version: " << e.cause();
+ }
+
+ m_parent->updateModVersions();
+}
+
+} // namespace ModPlatform
+
+/******** Helpers ********/
+auto ModPlatform::ListModel::hasFabric() const -> bool
+{
+ return !(dynamic_cast<MinecraftInstance*>((dynamic_cast<ModPage*>(parent()))->m_instance))
+ ->getPackProfile()
+ ->getComponentVersion("net.fabricmc.fabric-loader")
+ .isEmpty();
+}
+
+auto ModPlatform::ListModel::getMineVersions() const -> QList<QString>
+{
+ return { (dynamic_cast<MinecraftInstance*>((dynamic_cast<ModPage*>(parent()))->m_instance))
+ ->getPackProfile()
+ ->getComponentVersion("net.minecraft") };
+}
diff --git a/launcher/ui/pages/modplatform/ModModel.h b/launcher/ui/pages/modplatform/ModModel.h
new file mode 100644
index 00000000..64cfa71e
--- /dev/null
+++ b/launcher/ui/pages/modplatform/ModModel.h
@@ -0,0 +1,85 @@
+#pragma once
+
+#include <QAbstractListModel>
+
+#include "modplatform/ModAPI.h"
+#include "modplatform/ModIndex.h"
+#include "net/NetJob.h"
+
+class ModPage;
+
+namespace ModPlatform {
+
+using LogoMap = QMap<QString, QIcon>;
+using LogoCallback = std::function<void (QString)>;
+
+class ListModel : public QAbstractListModel {
+ Q_OBJECT
+
+ public:
+ ListModel(ModPage* parent);
+ ~ListModel() override = default;
+
+ inline auto rowCount(const QModelIndex& parent) const -> int override { return modpacks.size(); };
+ inline auto columnCount(const QModelIndex& parent) const -> int override { return 1; };
+ inline auto flags(const QModelIndex& index) const -> Qt::ItemFlags override { return QAbstractListModel::flags(index); };
+
+ auto debugName() const -> QString;
+
+ /* Retrieve information from the model at a given index with the given role */
+ auto data(const QModelIndex& index, int role) const -> QVariant override;
+
+ inline void setActiveJob(NetJob::Ptr ptr) { jobPtr = ptr; }
+
+ /* Ask the API for more information */
+ void fetchMore(const QModelIndex& parent) override;
+ void searchWithTerm(const QString& term, const int sort);
+ void requestModVersions(const ModPlatform::IndexedPack& current);
+
+ virtual void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) = 0;
+ virtual void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) = 0;
+
+ void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback);
+
+ inline auto canFetchMore(const QModelIndex& parent) const -> bool override { return searchState == CanPossiblyFetchMore; };
+
+ public slots:
+ void searchRequestFinished(QJsonDocument& doc);
+ void searchRequestFailed(QString reason);
+
+ void versionRequestSucceeded(QJsonDocument doc, QString addonId);
+
+ protected slots:
+
+ void logoFailed(QString logo);
+ void logoLoaded(QString logo, QIcon out);
+
+ void performPaginatedSearch();
+
+ protected:
+ virtual auto documentToArray(QJsonDocument& obj) const -> QJsonArray = 0;
+ virtual auto getSorts() const -> const char** = 0;
+
+ void requestLogo(QString file, QString url);
+
+ inline auto hasFabric() const -> bool;
+ inline auto getMineVersions() const -> QList<QString>;
+
+ protected:
+ ModPage* m_parent;
+
+ QList<ModPlatform::IndexedPack> modpacks;
+
+ LogoMap m_logoMap;
+ QMap<QString, LogoCallback> waitingCallbacks;
+ QStringList m_failedLogos;
+ QStringList m_loadingLogos;
+
+ QString currentSearchTerm;
+ int currentSort = 0;
+ int nextSearchOffset = 0;
+ enum SearchState { None, CanPossiblyFetchMore, ResetRequested, Finished } searchState = None;
+
+ NetJob::Ptr jobPtr;
+};
+} // namespace ModPlatform
diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp
new file mode 100644
index 00000000..3a116d3c
--- /dev/null
+++ b/launcher/ui/pages/modplatform/ModPage.cpp
@@ -0,0 +1,170 @@
+#include "ModPage.h"
+#include "ui_ModPage.h"
+
+#include <QKeyEvent>
+
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/PackProfile.h"
+#include "ui/dialogs/ModDownloadDialog.h"
+
+ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api)
+ : QWidget(dialog), m_instance(instance), ui(new Ui::ModPage), dialog(dialog), api(api)
+{
+ ui->setupUi(this);
+ connect(ui->searchButton, &QPushButton::clicked, this, &ModPage::triggerSearch);
+ ui->searchEdit->installEventFilter(this);
+
+ ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
+ ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300);
+
+}
+
+ModPage::~ModPage()
+{
+ delete ui;
+}
+
+
+/******** Qt things ********/
+
+void ModPage::openedImpl()
+{
+ updateSelectionButton();
+ triggerSearch();
+}
+
+auto ModPage::eventFilter(QObject* watched, QEvent* event) -> bool
+{
+ if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) {
+ auto* keyEvent = dynamic_cast<QKeyEvent*>(event);
+ if (keyEvent->key() == Qt::Key_Return) {
+ triggerSearch();
+ keyEvent->accept();
+ return true;
+ }
+ }
+ return QWidget::eventFilter(watched, event);
+}
+
+
+/******** Callbacks to events in the UI (set up in the derived classes) ********/
+
+void ModPage::triggerSearch()
+{
+ listModel->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex());
+}
+
+void ModPage::onSelectionChanged(QModelIndex first, QModelIndex second)
+{
+ ui->versionSelectionBox->clear();
+
+ if (!first.isValid()) { return; }
+
+ current = listModel->data(first, Qt::UserRole).value<ModPlatform::IndexedPack>();
+ QString text = "";
+ QString name = current.name;
+
+ if (current.websiteUrl.isEmpty())
+ text = name;
+ else
+ text = "<a href=\"" + current.websiteUrl + "\">" + name + "</a>";
+
+ if (!current.authors.empty()) {
+ auto authorToStr = [](ModPlatform::ModpackAuthor& author) -> QString {
+ if (author.url.isEmpty()) { return author.name; }
+ return QString("<a href=\"%1\">%2</a>").arg(author.url, author.name);
+ };
+ QStringList authorStrs;
+ for (auto& author : current.authors) {
+ authorStrs.push_back(authorToStr(author));
+ }
+ text += "<br>" + tr(" by ") + authorStrs.join(", ");
+ }
+ text += "<br><br>";
+
+ ui->packDescription->setHtml(text + current.description);
+
+ if (!current.versionsLoaded) {
+ qDebug() << QString("Loading %1 mod versions").arg(debugName());
+
+ ui->modSelectionButton->setText(tr("Loading versions..."));
+ ui->modSelectionButton->setEnabled(false);
+
+ listModel->requestModVersions(current);
+ } else {
+ for (int i = 0; i < current.versions.size(); i++) {
+ ui->versionSelectionBox->addItem(current.versions[i].version, QVariant(i));
+ }
+ if (ui->versionSelectionBox->count() == 0) { ui->versionSelectionBox->addItem(tr("No valid version found."), QVariant(-1)); }
+
+ updateSelectionButton();
+ }
+}
+
+void ModPage::onVersionSelectionChanged(QString data)
+{
+ if (data.isNull() || data.isEmpty()) {
+ selectedVersion = -1;
+ return;
+ }
+ selectedVersion = ui->versionSelectionBox->currentData().toInt();
+ updateSelectionButton();
+}
+
+void ModPage::onModSelected()
+{
+ auto& version = current.versions[selectedVersion];
+ if (dialog->isModSelected(current.name, version.fileName)) {
+ dialog->removeSelectedMod(current.name);
+ } else {
+ dialog->addSelectedMod(current.name, new ModDownloadTask(version.downloadUrl, version.fileName, dialog->mods));
+ }
+
+ updateSelectionButton();
+}
+
+
+/******** Make changes to the UI ********/
+
+void ModPage::retranslate()
+{
+ ui->retranslateUi(this);
+}
+
+void ModPage::updateModVersions()
+{
+ auto packProfile = (dynamic_cast<MinecraftInstance*>(m_instance))->getPackProfile();
+
+ QString mcVersion = packProfile->getComponentVersion("net.minecraft");
+ QString loaderString = (packProfile->getComponentVersion("net.minecraftforge").isEmpty()) ? "fabric" : "forge";
+
+ for (int i = 0; i < current.versions.size(); i++) {
+ auto version = current.versions[i];
+ //NOTE: Flame doesn't care about loaderString, so passing it changes nothing.
+ if (!validateVersion(version, mcVersion, loaderString)) {
+ continue;
+ }
+ ui->versionSelectionBox->addItem(version.version, QVariant(i));
+ }
+ if (ui->versionSelectionBox->count() == 0) { ui->versionSelectionBox->addItem(tr("No valid version found!"), QVariant(-1)); }
+
+ ui->modSelectionButton->setText(tr("Cannot select invalid version :("));
+ updateSelectionButton();
+}
+
+
+void ModPage::updateSelectionButton()
+{
+ if (!isOpened || selectedVersion < 0) {
+ ui->modSelectionButton->setEnabled(false);
+ return;
+ }
+
+ ui->modSelectionButton->setEnabled(true);
+ auto& version = current.versions[selectedVersion];
+ if (!dialog->isModSelected(current.name, version.fileName)) {
+ ui->modSelectionButton->setText(tr("Select mod for download"));
+ } else {
+ ui->modSelectionButton->setText(tr("Deselect mod for download"));
+ }
+}
diff --git a/launcher/ui/pages/modplatform/ModPage.h b/launcher/ui/pages/modplatform/ModPage.h
new file mode 100644
index 00000000..0cd13f37
--- /dev/null
+++ b/launcher/ui/pages/modplatform/ModPage.h
@@ -0,0 +1,69 @@
+#pragma once
+
+#include <QWidget>
+
+#include "Application.h"
+#include "modplatform/ModAPI.h"
+#include "modplatform/ModIndex.h"
+#include "ui/pages/BasePage.h"
+#include "ui/pages/modplatform/ModModel.h"
+
+class ModDownloadDialog;
+
+namespace Ui {
+class ModPage;
+}
+
+/* This page handles most logic related to browsing and selecting mods to download. */
+class ModPage : public QWidget, public BasePage {
+ Q_OBJECT
+
+ public:
+ explicit ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api);
+ ~ModPage() override;
+
+ /* Affects what the user sees */
+ auto displayName() const -> QString override = 0;
+ auto icon() const -> QIcon override = 0;
+ auto id() const -> QString override = 0;
+ auto helpPage() const -> QString override = 0;
+
+ /* Used internally */
+ virtual auto metaEntryBase() const -> QString = 0;
+ virtual auto debugName() const -> QString = 0;
+
+
+ void retranslate() override;
+
+ auto shouldDisplay() const -> bool override = 0;
+ virtual auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, QString loaderVer = "") const -> bool = 0;
+
+ auto apiProvider() const -> const ModAPI* { return api.get(); };
+
+ auto getCurrent() -> ModPlatform::IndexedPack& { return current; }
+ void updateModVersions();
+
+ void openedImpl() override;
+ auto eventFilter(QObject* watched, QEvent* event) -> bool override;
+
+ BaseInstance* m_instance;
+
+ protected:
+ void updateSelectionButton();
+
+ protected slots:
+ void triggerSearch();
+ void onSelectionChanged(QModelIndex first, QModelIndex second);
+ void onVersionSelectionChanged(QString data);
+ void onModSelected();
+
+ protected:
+ Ui::ModPage* ui = nullptr;
+ ModDownloadDialog* dialog = nullptr;
+ ModPlatform::ListModel* listModel = nullptr;
+ ModPlatform::IndexedPack current;
+
+ std::unique_ptr<ModAPI> api;
+
+ int selectedVersion = -1;
+};
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui b/launcher/ui/pages/modplatform/ModPage.ui
index 6c709825..508f1bac 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui
+++ b/launcher/ui/pages/modplatform/ModPage.ui
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
- <class>ModrinthPage</class>
- <widget class="QWidget" name="ModrinthPage">
+ <class>ModPage</class>
+ <widget class="QWidget" name="ModPage">
<property name="geometry">
<rect>
<x>0</x>
diff --git a/launcher/ui/pages/modplatform/flame/FlameModModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModModel.cpp
index e8afba5a..905fb2dd 100644
--- a/launcher/ui/pages/modplatform/flame/FlameModModel.cpp
+++ b/launcher/ui/pages/modplatform/flame/FlameModModel.cpp
@@ -1,273 +1,25 @@
#include "FlameModModel.h"
-#include "Application.h"
-#include "minecraft/MinecraftInstance.h"
-#include "minecraft/PackProfile.h"
-#include "FlameModPage.h"
-#include <Json.h>
-
-#include <MMCStrings.h>
-#include <Version.h>
-
-#include <QtMath>
+#include "modplatform/flame/FlameModIndex.h"
namespace FlameMod {
-ListModel::ListModel(FlameModPage *parent) : QAbstractListModel(parent)
-{
-}
-
-ListModel::~ListModel()
-{
-}
+// NOLINTNEXTLINE(modernize-avoid-c-arrays)
+const char* ListModel::sorts[6]{ "Featured", "Popularity", "LastUpdated", "Name", "Author", "TotalDownloads" };
-int ListModel::rowCount(const QModelIndex &parent) const
+void ListModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj)
{
- return modpacks.size();
-}
-
-int ListModel::columnCount(const QModelIndex &parent) const
-{
- return 1;
-}
-
-QVariant ListModel::data(const QModelIndex &index, int role) const
-{
- int pos = index.row();
- if(pos >= modpacks.size() || pos < 0 || !index.isValid())
- {
- return QString("INVALID INDEX %1").arg(pos);
- }
-
- IndexedPack pack = modpacks.at(pos);
- if(role == Qt::DisplayRole)
- {
- return pack.name;
- }
- else if (role == Qt::ToolTipRole)
- {
- if(pack.description.length() > 100)
- {
- //some magic to prevent to long tooltips and replace html linebreaks
- QString edit = pack.description.left(97);
- edit = edit.left(edit.lastIndexOf("<br>")).left(edit.lastIndexOf(" ")).append("...");
- return edit;
-
- }
- return pack.description;
- }
- else if(role == Qt::DecorationRole)
- {
- if(m_logoMap.contains(pack.logoName))
- {
- return (m_logoMap.value(pack.logoName));
- }
- QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder");
- ((ListModel *)this)->requestLogo(pack.logoName, pack.logoUrl);
- return icon;
- }
- else if(role == Qt::UserRole)
- {
- QVariant v;
- v.setValue(pack);
- return v;
- }
-
- return QVariant();
-}
-
-void ListModel::logoLoaded(QString logo, QIcon out)
-{
- m_loadingLogos.removeAll(logo);
- m_logoMap.insert(logo, out);
- for(int i = 0; i < modpacks.size(); i++) {
- if(modpacks[i].logoName == logo) {
- emit dataChanged(createIndex(i, 0), createIndex(i, 0), {Qt::DecorationRole});
- }
- }
-}
-
-void ListModel::logoFailed(QString logo)
-{
- m_failedLogos.append(logo);
- m_loadingLogos.removeAll(logo);
-}
-
-void ListModel::requestLogo(QString logo, QString url)
-{
- if(m_loadingLogos.contains(logo) || m_failedLogos.contains(logo))
- {
- return;
- }
-
- MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("FlameMods", QString("logos/%1").arg(logo.section(".", 0, 0)));
- auto job = new NetJob(QString("Flame Icon Download %1").arg(logo), APPLICATION->network());
- job->addNetAction(Net::Download::makeCached(QUrl(url), entry));
-
- auto fullPath = entry->getFullPath();
- QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job]
- {
- job->deleteLater();
- emit logoLoaded(logo, QIcon(fullPath));
- if(waitingCallbacks.contains(logo))
- {
- waitingCallbacks.value(logo)(fullPath);
- }
- });
-
- QObject::connect(job, &NetJob::failed, this, [this, logo, job]
- {
- job->deleteLater();
- emit logoFailed(logo);
- });
-
- job->start();
- m_loadingLogos.append(logo);
+ FlameMod::loadIndexedPack(m, obj);
}
-void ListModel::getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback)
+void ListModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
{
- if(m_logoMap.contains(logo))
- {
- callback(APPLICATION->metacache()->resolveEntry("FlameMods", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath());
- }
- else
- {
- requestLogo(logo, logoUrl);
- }
+ FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), m_parent->m_instance);
}
-Qt::ItemFlags ListModel::flags(const QModelIndex &index) const
+auto ListModel::documentToArray(QJsonDocument& obj) const -> QJsonArray
{
- return QAbstractListModel::flags(index);
-}
-
-bool ListModel::canFetchMore(const QModelIndex& parent) const
-{
- return searchState == CanPossiblyFetchMore;
-}
-
-void ListModel::fetchMore(const QModelIndex& parent)
-{
- if (parent.isValid())
- return;
- if(nextSearchOffset == 0) {
- qWarning() << "fetchMore with 0 offset is wrong...";
- return;
- }
- performPaginatedSearch();
-}
-const char* sorts[6]{"Featured","Popularity","LastUpdated","Name","Author","TotalDownloads"};
-
-void ListModel::performPaginatedSearch()
-{
-
- QString mcVersion = ((MinecraftInstance *)((FlameModPage *)parent())->m_instance)->getPackProfile()->getComponentVersion("net.minecraft");
- bool hasFabric = !((MinecraftInstance *)((FlameModPage *)parent())->m_instance)->getPackProfile()->getComponentVersion("net.fabricmc.fabric-loader").isEmpty();
- auto netJob = new NetJob("Flame::Search", APPLICATION->network());
- auto searchUrl = QString(
- "https://addons-ecs.forgesvc.net/api/v2/addon/search?"
- "gameId=432&"
- "categoryId=0&"
- "sectionId=6&"
-
- "index=%1&"
- "pageSize=25&"
- "searchFilter=%2&"
- "sort=%3&"
- "modLoaderType=%4&"
- "gameVersion=%5"
- )
- .arg(nextSearchOffset)
- .arg(currentSearchTerm)
- .arg(sorts[currentSort])
- .arg(hasFabric ? 4 : 1) // Enum: https://docs.curseforge.com/?http#tocS_ModLoaderType
- .arg(mcVersion);
-
- netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response));
- jobPtr = netJob;
- jobPtr->start();
- QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::searchRequestFinished);
- QObject::connect(netJob, &NetJob::failed, this, &ListModel::searchRequestFailed);
-}
-
-void ListModel::searchWithTerm(const QString &term, const int sort)
-{
- if(currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort) {
- return;
- }
- currentSearchTerm = term;
- currentSort = sort;
- if(jobPtr) {
- jobPtr->abort();
- searchState = ResetRequested;
- return;
- }
- else {
- beginResetModel();
- modpacks.clear();
- endResetModel();
- searchState = None;
- }
- nextSearchOffset = 0;
- performPaginatedSearch();
-}
-
-void ListModel::searchRequestFinished()
-{
- jobPtr.reset();
-
- QJsonParseError parse_error;
- QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error);
- if(parse_error.error != QJsonParseError::NoError) {
- qWarning() << "Error while parsing JSON response from Flame at " << parse_error.offset << " reason: " << parse_error.errorString();
- qWarning() << response;
- return;
- }
-
- QList<FlameMod::IndexedPack> newList;
- auto packs = doc.array();
- for(auto packRaw : packs) {
- auto packObj = packRaw.toObject();
-
- FlameMod::IndexedPack pack;
- try
- {
- FlameMod::loadIndexedPack(pack, packObj);
- newList.append(pack);
- }
- catch(const JSONValidationError &e)
- {
- qWarning() << "Error while loading mod from Flame: " << e.cause();
- continue;
- }
- }
- if(packs.size() < 25) {
- searchState = Finished;
- } else {
- nextSearchOffset += 25;
- searchState = CanPossiblyFetchMore;
- }
- beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1);
- modpacks.append(newList);
- endInsertRows();
-}
-
-void ListModel::searchRequestFailed(QString reason)
-{
- jobPtr.reset();
-
- if(searchState == ResetRequested) {
- beginResetModel();
- modpacks.clear();
- endResetModel();
-
- nextSearchOffset = 0;
- performPaginatedSearch();
- } else {
- searchState = Finished;
- }
-}
-
+ return obj.array();
}
+} // namespace FlameMod
diff --git a/launcher/ui/pages/modplatform/flame/FlameModModel.h b/launcher/ui/pages/modplatform/flame/FlameModModel.h
index 0c1cb95e..707c1bb1 100644
--- a/launcher/ui/pages/modplatform/flame/FlameModModel.h
+++ b/launcher/ui/pages/modplatform/flame/FlameModModel.h
@@ -1,79 +1,25 @@
#pragma once
-#include <RWStorage.h>
-
-#include <QAbstractListModel>
-#include <QSortFilterProxyModel>
-#include <QThreadPool>
-#include <QIcon>
-#include <QStyledItemDelegate>
-#include <QList>
-#include <QString>
-#include <QStringList>
-#include <QMetaType>
-
-#include <functional>
-#include <net/NetJob.h>
-
-#include <modplatform/flame/FlamePackIndex.h>
-#include "modplatform/flame/FlameModIndex.h"
-#include "BaseInstance.h"
#include "FlameModPage.h"
namespace FlameMod {
-
-typedef QMap<QString, QIcon> LogoMap;
-typedef std::function<void(QString)> LogoCallback;
-
-class ListModel : public QAbstractListModel
-{
+class ListModel : public ModPlatform::ListModel {
Q_OBJECT
-public:
- ListModel(FlameModPage *parent);
- virtual ~ListModel();
-
- int rowCount(const QModelIndex &parent) const override;
- int columnCount(const QModelIndex &parent) const override;
- QVariant data(const QModelIndex &index, int role) const override;
- Qt::ItemFlags flags(const QModelIndex &index) const override;
- bool canFetchMore(const QModelIndex & parent) const override;
- void fetchMore(const QModelIndex & parent) override;
-
- void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback);
- void searchWithTerm(const QString &term, const int sort);
-
-private slots:
- void performPaginatedSearch();
-
- void logoFailed(QString logo);
- void logoLoaded(QString logo, QIcon out);
-
- void searchRequestFinished();
- void searchRequestFailed(QString reason);
+ public:
+ ListModel(FlameModPage* parent) : ModPlatform::ListModel(parent) {}
+ ~ListModel() override = default;
-private:
- void requestLogo(QString file, QString url);
+ private:
+ void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
+ void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override;
-private:
- QList<IndexedPack> modpacks;
- QStringList m_failedLogos;
- QStringList m_loadingLogos;
- LogoMap m_logoMap;
- QMap<QString, LogoCallback> waitingCallbacks;
+ auto documentToArray(QJsonDocument& obj) const -> QJsonArray override;
- QString currentSearchTerm;
- int currentSort = 0;
- int nextSearchOffset = 0;
- enum SearchState {
- None,
- CanPossiblyFetchMore,
- ResetRequested,
- Finished
- } searchState = None;
- NetJob::Ptr jobPtr;
- QByteArray response;
+ // NOLINTNEXTLINE(modernize-avoid-c-arrays)
+ static const char* sorts[6];
+ inline auto getSorts() const -> const char** override { return sorts; };
};
-}
+} // namespace FlameMod
diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp
index d1641729..864ae8e6 100644
--- a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp
+++ b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp
@@ -34,224 +34,40 @@
*/
#include "FlameModPage.h"
-#include "ui_FlameModPage.h"
+#include "ui_ModPage.h"
-#include <QKeyEvent>
-
-#include "Application.h"
#include "FlameModModel.h"
-#include "InstanceImportTask.h"
-#include "Json.h"
-#include "ModDownloadTask.h"
-#include "minecraft/MinecraftInstance.h"
-#include "minecraft/PackProfile.h"
#include "ui/dialogs/ModDownloadDialog.h"
-FlameModPage::FlameModPage(ModDownloadDialog *dialog, BaseInstance *instance)
- : QWidget(dialog), m_instance(instance), ui(new Ui::FlameModPage),
- dialog(dialog) {
- ui->setupUi(this);
- connect(ui->searchButton, &QPushButton::clicked, this,
- &FlameModPage::triggerSearch);
- ui->searchEdit->installEventFilter(this);
- listModel = new FlameMod::ListModel(this);
- ui->packView->setModel(listModel);
-
- ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(
- Qt::ScrollBarAsNeeded);
- ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300);
-
- // index is used to set the sorting with the flame api
- ui->sortByBox->addItem(tr("Sort by Featured"));
- ui->sortByBox->addItem(tr("Sort by Popularity"));
- ui->sortByBox->addItem(tr("Sort by last updated"));
- ui->sortByBox->addItem(tr("Sort by Name"));
- ui->sortByBox->addItem(tr("Sort by Author"));
- ui->sortByBox->addItem(tr("Sort by Downloads"));
-
- connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this,
- SLOT(triggerSearch()));
- connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged,
- this, &FlameModPage::onSelectionChanged);
- connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this,
- &FlameModPage::onVersionSelectionChanged);
- connect(ui->modSelectionButton, &QPushButton::clicked, this,
- &FlameModPage::onModSelected);
-}
-
-FlameModPage::~FlameModPage() { delete ui; }
-
-bool FlameModPage::eventFilter(QObject *watched, QEvent *event) {
- if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) {
- QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
- if (keyEvent->key() == Qt::Key_Return) {
- triggerSearch();
- keyEvent->accept();
- return true;
- }
- }
- return QWidget::eventFilter(watched, event);
-}
-
-bool FlameModPage::shouldDisplay() const { return true; }
-
-void FlameModPage::retranslate()
+FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance)
+ : ModPage(dialog, instance, new FlameAPI())
{
- ui->retranslateUi(this);
-}
-
-void FlameModPage::openedImpl() {
- updateSelectionButton();
- triggerSearch();
-}
-
-void FlameModPage::triggerSearch() {
- listModel->searchWithTerm(ui->searchEdit->text(),
- ui->sortByBox->currentIndex());
-}
-
-void FlameModPage::onSelectionChanged(QModelIndex first, QModelIndex second) {
- ui->versionSelectionBox->clear();
-
- if (!first.isValid()) {
- return;
- }
-
- current = listModel->data(first, Qt::UserRole).value<FlameMod::IndexedPack>();
- QString text = "";
- QString name = current.name;
-
- if (current.websiteUrl.isEmpty())
- text = name;
- else
- text = "<a href=\"" + current.websiteUrl + "\">" + name + "</a>";
- if (!current.authors.empty()) {
- auto authorToStr = [](FlameMod::ModpackAuthor &author) {
- if (author.url.isEmpty()) {
- return author.name;
- }
- return QString("<a href=\"%1\">%2</a>").arg(author.url, author.name);
- };
- QStringList authorStrs;
- for (auto &author : current.authors) {
- authorStrs.push_back(authorToStr(author));
- }
- text += "<br>" + tr(" by ") + authorStrs.join(", ");
- }
- text += "<br><br>";
-
- ui->packDescription->setHtml(text + current.description);
-
- if (!current.versionsLoaded) {
- qDebug() << "Loading flame mod versions";
-
- ui->modSelectionButton->setText(tr("Loading versions..."));
- ui->modSelectionButton->setEnabled(false);
-
- auto netJob =
- new NetJob(QString("Flame::ModVersions(%1)").arg(current.name),
- APPLICATION->network());
- auto response = new QByteArray();
- int addonId = current.addonId;
- netJob->addNetAction(Net::Download::makeByteArray(
- QString("https://addons-ecs.forgesvc.net/api/v2/addon/%1/files")
- .arg(addonId),
- response));
-
- QObject::connect(netJob, &NetJob::succeeded, this, [this, response, addonId] {
- if(addonId != current.addonId){
- return; //wrong request
- }
- QJsonParseError parse_error;
- QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
- if (parse_error.error != QJsonParseError::NoError) {
- qWarning() << "Error while parsing JSON response from Flame at "
- << parse_error.offset
- << " reason: " << parse_error.errorString();
- qWarning() << *response;
- return;
- }
- QJsonArray arr = doc.array();
- try {
- FlameMod::loadIndexedPackVersions(current, arr, APPLICATION->network(),
- m_instance);
- } catch (const JSONValidationError &e) {
- qDebug() << *response;
- qWarning() << "Error while reading Flame mod version: " << e.cause();
- }
- auto packProfile = ((MinecraftInstance *)m_instance)->getPackProfile();
- QString mcVersion = packProfile->getComponentVersion("net.minecraft");
- QString loaderString =
- (packProfile->getComponentVersion("net.minecraftforge").isEmpty())
- ? "fabric"
- : "forge";
- for (int i = 0; i < current.versions.size(); i++) {
- auto version = current.versions[i];
- if (!version.mcVersion.contains(mcVersion)) {
- continue;
- }
- ui->versionSelectionBox->addItem(version.version, QVariant(i));
- }
- if (ui->versionSelectionBox->count() == 0) {
- ui->versionSelectionBox->addItem(tr("No valid version found."),
- QVariant(-1));
- }
-
- ui->modSelectionButton->setText(tr("Cannot select invalid version :("));
- updateSelectionButton();
- });
- QObject::connect(netJob, &NetJob::finished, this, [response, netJob] {
- netJob->deleteLater();
- delete response;
- });
- netJob->start();
- } else {
- for (int i = 0; i < current.versions.size(); i++) {
- ui->versionSelectionBox->addItem(current.versions[i].version,
- QVariant(i));
- }
- if (ui->versionSelectionBox->count() == 0) {
- ui->versionSelectionBox->addItem(tr("No valid version found."),
- QVariant(-1));
- }
-
- updateSelectionButton();
- }
-}
-
-void FlameModPage::updateSelectionButton() {
- if (!isOpened || selectedVersion < 0) {
- ui->modSelectionButton->setEnabled(false);
- return;
- }
-
- ui->modSelectionButton->setEnabled(true);
- auto &version = current.versions[selectedVersion];
- if (!dialog->isModSelected(current.name, version.fileName)) {
- ui->modSelectionButton->setText(tr("Select mod for download"));
- } else {
- ui->modSelectionButton->setText(tr("Deselect mod for download"));
- }
+ listModel = new FlameMod::ListModel(this);
+ ui->packView->setModel(listModel);
+
+ // index is used to set the sorting with the flame api
+ ui->sortByBox->addItem(tr("Sort by Featured"));
+ ui->sortByBox->addItem(tr("Sort by Popularity"));
+ ui->sortByBox->addItem(tr("Sort by last updated"));
+ ui->sortByBox->addItem(tr("Sort by Name"));
+ ui->sortByBox->addItem(tr("Sort by Author"));
+ ui->sortByBox->addItem(tr("Sort by Downloads"));
+
+ // sometimes Qt just ignores virtual slots and doesn't work as intended it seems,
+ // so it's best not to connect them in the parent's contructor...
+ connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch()));
+ connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlameModPage::onSelectionChanged);
+ connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FlameModPage::onVersionSelectionChanged);
+ connect(ui->modSelectionButton, &QPushButton::clicked, this, &FlameModPage::onModSelected);
}
-void FlameModPage::onVersionSelectionChanged(QString data) {
- if (data.isNull() || data.isEmpty()) {
- selectedVersion = -1;
- return;
- }
- selectedVersion = ui->versionSelectionBox->currentData().toInt();
- updateSelectionButton();
+auto FlameModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, QString loaderVer) const -> bool
+{
+ (void) loaderVer;
+ return ver.mcVersion.contains(mineVer);
}
-void FlameModPage::onModSelected() {
- auto &version = current.versions[selectedVersion];
- if (dialog->isModSelected(current.name, version.fileName)) {
- dialog->removeSelectedMod(current.name);
- } else {
- dialog->addSelectedMod(current.name,
- new ModDownloadTask(version.downloadUrl,
- version.fileName, dialog->mods));
- }
-
- updateSelectionButton();
-}
+// I don't know why, but doing this on the parent class makes it so that
+// other mod providers start loading before being selected, at least with
+// my Qt, so we need to implement this in every derived class...
+auto FlameModPage::shouldDisplay() const -> bool { return true; }
diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.h b/launcher/ui/pages/modplatform/flame/FlameModPage.h
index 2a6ade85..dc58fd7f 100644
--- a/launcher/ui/pages/modplatform/flame/FlameModPage.h
+++ b/launcher/ui/pages/modplatform/flame/FlameModPage.h
@@ -35,70 +35,26 @@
#pragma once
-#include <QWidget>
+#include "ui/pages/modplatform/ModPage.h"
-#include "ui/pages/BasePage.h"
-#include <Application.h>
-#include "tasks/Task.h"
-#include "modplatform/flame/FlameModIndex.h"
+#include "modplatform/flame/FlameAPI.h"
-namespace Ui
-{
-class FlameModPage;
-}
-
-class ModDownloadDialog;
-
-namespace FlameMod {
- class ListModel;
-}
-
-class FlameModPage : public QWidget, public BasePage
-{
+class FlameModPage : public ModPage {
Q_OBJECT
-public:
- explicit FlameModPage(ModDownloadDialog *dialog, BaseInstance *instance);
- virtual ~FlameModPage();
- virtual QString displayName() const override
- {
- return "CurseForge";
- }
- virtual QIcon icon() const override
- {
- return APPLICATION->getThemedIcon("flame");
- }
- virtual QString id() const override
- {
- return "curseforge";
- }
- virtual QString helpPage() const override
- {
- return "Flame-platform";
- }
- virtual bool shouldDisplay() const override;
- void retranslate() override;
-
- void openedImpl() override;
-
- bool eventFilter(QObject * watched, QEvent * event) override;
-
- BaseInstance *m_instance;
+ public:
+ explicit FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance);
+ ~FlameModPage() override = default;
-private:
- void updateSelectionButton();
+ inline auto displayName() const -> QString override { return "CurseForge"; }
+ inline auto icon() const -> QIcon override { return APPLICATION->getThemedIcon("flame"); }
+ inline auto id() const -> QString override { return "curseforge"; }
+ inline auto helpPage() const -> QString override { return "Flame-platform"; }
-private slots:
- void triggerSearch();
- void onSelectionChanged(QModelIndex first, QModelIndex second);
- void onVersionSelectionChanged(QString data);
- void onModSelected();
+ inline auto debugName() const -> QString override { return "Flame"; }
+ inline auto metaEntryBase() const -> QString override { return "FlameMods"; };
-private:
- Ui::FlameModPage *ui = nullptr;
- ModDownloadDialog* dialog = nullptr;
- FlameMod::ListModel* listModel = nullptr;
- FlameMod::IndexedPack current;
+ auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, QString loaderVer = "") const -> bool override;
- int selectedVersion = -1;
+ auto shouldDisplay() const -> bool override;
};
diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.ui b/launcher/ui/pages/modplatform/flame/FlameModPage.ui
deleted file mode 100644
index 25cb2571..00000000
--- a/launcher/ui/pages/modplatform/flame/FlameModPage.ui
+++ /dev/null
@@ -1,97 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<ui version="4.0">
- <class>FlameModPage</class>
- <widget class="QWidget" name="FlameModPage">
- <property name="geometry">
- <rect>
- <x>0</x>
- <y>0</y>
- <width>837</width>
- <height>685</height>
- </rect>
- </property>
- <layout class="QGridLayout" name="gridLayout">
- <item row="2" column="0" colspan="2">
- <layout class="QGridLayout" name="gridLayout_4" columnstretch="0,0,0" rowminimumheight="0,0,0" columnminimumwidth="0,0,0">
- <item row="1" column="2">
- <widget class="QComboBox" name="versionSelectionBox"/>
- </item>
- <item row="1" column="0">
- <widget class="QComboBox" name="sortByBox"/>
- </item>
- <item row="1" column="1">
- <widget class="QLabel" name="label">
- <property name="text">
- <string>Version selected:</string>
- </property>
- <property name="alignment">
- <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
- </property>
- </widget>
- </item>
- <item row="2" column="2">
- <widget class="QPushButton" name="modSelectionButton">
- <property name="text">
- <string>Select mod for download</string>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item row="0" column="0">
- <widget class="QLineEdit" name="searchEdit">
- <property name="placeholderText">
- <string>Search and filter...</string>
- </property>
- </widget>
- </item>
- <item row="1" column="0" colspan="2">
- <layout class="QGridLayout" name="gridLayout_3">
- <item row="1" column="0">
- <widget class="QListView" name="packView">
- <property name="horizontalScrollBarPolicy">
- <enum>Qt::ScrollBarAlwaysOff</enum>
- </property>
- <property name="alternatingRowColors">
- <bool>true</bool>
- </property>
- <property name="iconSize">
- <size>
- <width>48</width>
- <height>48</height>
- </size>
- </property>
- </widget>
- </item>
- <item row="1" column="1">
- <widget class="QTextBrowser" name="packDescription">
- <property name="openExternalLinks">
- <bool>true</bool>
- </property>
- <property name="openLinks">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item row="0" column="1">
- <widget class="QPushButton" name="searchButton">
- <property name="text">
- <string>Search</string>
- </property>
- </widget>
- </item>
- </layout>
- </widget>
- <tabstops>
- <tabstop>searchEdit</tabstop>
- <tabstop>searchButton</tabstop>
- <tabstop>packView</tabstop>
- <tabstop>packDescription</tabstop>
- <tabstop>sortByBox</tabstop>
- <tabstop>versionSelectionBox</tabstop>
- </tabstops>
- <resources/>
- <connections/>
-</ui>
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp
index 5a18830a..b788860a 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp
@@ -1,276 +1,43 @@
-#include "ModrinthModel.h"
-#include "Application.h"
-#include "minecraft/MinecraftInstance.h"
-#include "minecraft/PackProfile.h"
-#include "ModrinthPage.h"
-#include "ui/dialogs/ModDownloadDialog.h"
-#include <Json.h>
-
-#include <MMCStrings.h>
-#include <Version.h>
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * PolyMC - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
-#include <QtMath>
-#include <QMessageBox>
+#include "ModrinthModel.h"
+#include "modplatform/modrinth/ModrinthPackIndex.h"
namespace Modrinth {
-ListModel::ListModel(ModrinthPage *parent) : QAbstractListModel(parent)
-{
-}
-
-ListModel::~ListModel()
-{
-}
+// NOLINTNEXTLINE(modernize-avoid-c-arrays)
+const char* ListModel::sorts[5]{ "relevance", "downloads", "follows", "updated", "newest" };
-int ListModel::rowCount(const QModelIndex &parent) const
+void ListModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj)
{
- return modpacks.size();
+ Modrinth::loadIndexedPack(m, obj);
}
-int ListModel::columnCount(const QModelIndex &parent) const
+void ListModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
{
- return 1;
+ Modrinth::loadIndexedPackVersions(m, arr, APPLICATION->network(), m_parent->m_instance);
}
-QVariant ListModel::data(const QModelIndex &index, int role) const
+auto ListModel::documentToArray(QJsonDocument& obj) const -> QJsonArray
{
- int pos = index.row();
- if(pos >= modpacks.size() || pos < 0 || !index.isValid())
- {
- return QString("INVALID INDEX %1").arg(pos);
- }
-
- IndexedPack pack = modpacks.at(pos);
- if(role == Qt::DisplayRole)
- {
- return pack.name;
- }
- else if (role == Qt::ToolTipRole)
- {
- if(pack.description.length() > 100)
- {
- //some magic to prevent to long tooltips and replace html linebreaks
- QString edit = pack.description.left(97);
- edit = edit.left(edit.lastIndexOf("<br>")).left(edit.lastIndexOf(" ")).append("...");
- return edit;
-
- }
- return pack.description;
- }
- else if(role == Qt::DecorationRole)
- {
- if(m_logoMap.contains(pack.logoName))
- {
- return (m_logoMap.value(pack.logoName));
- }
- QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder");
- ((ListModel *)this)->requestLogo(pack.logoName, pack.logoUrl);
- return icon;
- }
- else if(role == Qt::UserRole)
- {
- QVariant v;
- v.setValue(pack);
- return v;
- }
-
- return QVariant();
-}
-
-void ListModel::logoLoaded(QString logo, QIcon out)
-{
- m_loadingLogos.removeAll(logo);
- m_logoMap.insert(logo, out);
- for(int i = 0; i < modpacks.size(); i++) {
- if(modpacks[i].logoName == logo) {
- emit dataChanged(createIndex(i, 0), createIndex(i, 0), {Qt::DecorationRole});
- }
- }
-}
-
-void ListModel::logoFailed(QString logo)
-{
- m_failedLogos.append(logo);
- m_loadingLogos.removeAll(logo);
-}
-
-void ListModel::requestLogo(QString logo, QString url)
-{
- if(m_loadingLogos.contains(logo) || m_failedLogos.contains(logo))
- {
- return;
- }
-
- MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("ModrinthPacks", QString("logos/%1").arg(logo.section(".", 0, 0)));
- auto job = new NetJob(QString("Modrinth Icon Download %1").arg(logo), APPLICATION->network());
- job->addNetAction(Net::Download::makeCached(QUrl(url), entry));
-
- auto fullPath = entry->getFullPath();
- QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job]
- {
- job->deleteLater();
- emit logoLoaded(logo, QIcon(fullPath));
- if(waitingCallbacks.contains(logo))
- {
- waitingCallbacks.value(logo)(fullPath);
- }
- });
-
- QObject::connect(job, &NetJob::failed, this, [this, logo, job]
- {
- job->deleteLater();
- emit logoFailed(logo);
- });
-
- job->start();
- m_loadingLogos.append(logo);
-}
-
-void ListModel::getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback)
-{
- if(m_logoMap.contains(logo))
- {
- callback(APPLICATION->metacache()->resolveEntry("ModrinthPacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath());
- }
- else
- {
- requestLogo(logo, logoUrl);
- }
-}
-
-Qt::ItemFlags ListModel::flags(const QModelIndex &index) const
-{
- return QAbstractListModel::flags(index);
-}
-
-bool ListModel::canFetchMore(const QModelIndex& parent) const
-{
- return searchState == CanPossiblyFetchMore;
-}
-
-void ListModel::fetchMore(const QModelIndex& parent)
-{
- if (parent.isValid())
- return;
- if(nextSearchOffset == 0) {
- qWarning() << "fetchMore with 0 offset is wrong...";
- return;
- }
- performPaginatedSearch();
-}
-const char* sorts[5]{"relevance","downloads","follows","updated","newest"};
-
-void ListModel::performPaginatedSearch()
-{
-
- QString mcVersion = ((MinecraftInstance *)((ModrinthPage *)parent())->m_instance)->getPackProfile()->getComponentVersion("net.minecraft");
- bool hasFabric = !((MinecraftInstance *)((ModrinthPage *)parent())->m_instance)->getPackProfile()->getComponentVersion("net.fabricmc.fabric-loader").isEmpty();
- auto netJob = new NetJob("Modrinth::Search", APPLICATION->network());
- auto searchUrl = QString(
- "https://api.modrinth.com/v2/search?"
- "offset=%1&"
- "limit=25&"
- "query=%2&"
- "index=%3&"
- "facets=[[\"categories:%4\"],[\"versions:%5\"],[\"project_type:mod\"]]"
- )
- .arg(nextSearchOffset)
- .arg(currentSearchTerm)
- .arg(sorts[currentSort])
- .arg(hasFabric ? "fabric" : "forge")
- .arg(mcVersion);
-
- netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response));
- jobPtr = netJob;
- jobPtr->start();
- QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::searchRequestFinished);
- QObject::connect(netJob, &NetJob::failed, this, &ListModel::searchRequestFailed);
-}
-
-void ListModel::searchWithTerm(const QString &term, const int sort)
-{
- if(currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort) {
- return;
- }
- currentSearchTerm = term;
- currentSort = sort;
- if(jobPtr) {
- jobPtr->abort();
- searchState = ResetRequested;
- return;
- }
- else {
- beginResetModel();
- modpacks.clear();
- endResetModel();
- searchState = None;
- }
- nextSearchOffset = 0;
- performPaginatedSearch();
-}
-
-void Modrinth::ListModel::searchRequestFinished()
-{
- jobPtr.reset();
-
- QJsonParseError parse_error;
- QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error);
- if(parse_error.error != QJsonParseError::NoError) {
- qWarning() << "Error while parsing JSON response from Modrinth at " << parse_error.offset << " reason: " << parse_error.errorString();
- qWarning() << response;
- return;
- }
-
- QList<Modrinth::IndexedPack> newList;
- auto packs = doc.object().value("hits").toArray();
- for(auto packRaw : packs) {
- auto packObj = packRaw.toObject();
-
- Modrinth::IndexedPack pack;
- try
- {
- Modrinth::loadIndexedPack(pack, packObj);
- newList.append(pack);
- }
- catch(const JSONValidationError &e)
- {
- qWarning() << "Error while loading mod from Modrinth: " << e.cause();
- continue;
- }
- }
- if(packs.size() < 25) {
- searchState = Finished;
- } else {
- nextSearchOffset += 25;
- searchState = CanPossiblyFetchMore;
- }
- beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1);
- modpacks.append(newList);
- endInsertRows();
-}
-
-void Modrinth::ListModel::searchRequestFailed(QString reason)
-{
- if(jobPtr->first()->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 409){
- //409 Gone, notify user to update
- QMessageBox::critical(nullptr, tr("Error"), tr("Modrinth API version too old!\nPlease update PolyMC!"));
- //self-destruct
- ((ModDownloadDialog *)((ModrinthPage *)parent())->parentWidget())->reject();
- }
- jobPtr.reset();
-
- if(searchState == ResetRequested) {
- beginResetModel();
- modpacks.clear();
- endResetModel();
-
- nextSearchOffset = 0;
- performPaginatedSearch();
- } else {
- searchState = Finished;
- }
-}
-
+ return obj.object().value("hits").toArray();
}
+} // namespace Modrinth
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h
index 53f1f134..45a6090a 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h
@@ -1,79 +1,25 @@
#pragma once
-#include <RWStorage.h>
-
-#include <QAbstractListModel>
-#include <QSortFilterProxyModel>
-#include <QThreadPool>
-#include <QIcon>
-#include <QStyledItemDelegate>
-#include <QList>
-#include <QString>
-#include <QStringList>
-#include <QMetaType>
-
-#include <functional>
-#include <net/NetJob.h>
-
-#include <modplatform/flame/FlamePackIndex.h>
-#include "modplatform/modrinth/ModrinthPackIndex.h"
-#include "BaseInstance.h"
#include "ModrinthPage.h"
namespace Modrinth {
-
-typedef QMap<QString, QIcon> LogoMap;
-typedef std::function<void(QString)> LogoCallback;
-
-class ListModel : public QAbstractListModel
-{
+class ListModel : public ModPlatform::ListModel {
Q_OBJECT
-public:
- ListModel(ModrinthPage *parent);
- virtual ~ListModel();
-
- int rowCount(const QModelIndex &parent) const override;
- int columnCount(const QModelIndex &parent) const override;
- QVariant data(const QModelIndex &index, int role) const override;
- Qt::ItemFlags flags(const QModelIndex &index) const override;
- bool canFetchMore(const QModelIndex & parent) const override;
- void fetchMore(const QModelIndex & parent) override;
-
- void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback);
- void searchWithTerm(const QString &term, const int sort);
-
-private slots:
- void performPaginatedSearch();
-
- void logoFailed(QString logo);
- void logoLoaded(QString logo, QIcon out);
-
- void searchRequestFinished();
- void searchRequestFailed(QString reason);
-
-private:
- void requestLogo(QString file, QString url);
+ public:
+ ListModel(ModrinthPage* parent) : ModPlatform::ListModel(parent){};
+ ~ListModel() override = default;
-private:
- QList<IndexedPack> modpacks;
- QStringList m_failedLogos;
- QStringList m_loadingLogos;
- LogoMap m_logoMap;
- QMap<QString, LogoCallback> waitingCallbacks;
+ private:
+ void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
+ void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override;
+
+ auto documentToArray(QJsonDocument& obj) const -> QJsonArray override;
- QString currentSearchTerm;
- int currentSort = 0;
- int nextSearchOffset = 0;
- enum SearchState {
- None,
- CanPossiblyFetchMore,
- ResetRequested,
- Finished
- } searchState = None;
- NetJob::Ptr jobPtr;
- QByteArray response;
+ // NOLINTNEXTLINE(modernize-avoid-c-arrays)
+ static const char* sorts[5];
+ inline auto getSorts() const -> const char** override { return sorts; };
};
-}
+} // namespace Modrinth
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp
index 82340448..ddaf96e2 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp
@@ -34,211 +34,38 @@
*/
#include "ModrinthPage.h"
-#include "ui_ModrinthPage.h"
+#include "ui_ModPage.h"
-#include <QKeyEvent>
-
-#include "Application.h"
-#include "InstanceImportTask.h"
-#include "Json.h"
-#include "ModDownloadTask.h"
#include "ModrinthModel.h"
-#include "minecraft/MinecraftInstance.h"
-#include "minecraft/PackProfile.h"
#include "ui/dialogs/ModDownloadDialog.h"
-ModrinthPage::ModrinthPage(ModDownloadDialog *dialog, BaseInstance *instance)
- : QWidget(dialog), m_instance(instance), ui(new Ui::ModrinthPage),
- dialog(dialog) {
- ui->setupUi(this);
- connect(ui->searchButton, &QPushButton::clicked, this,
- &ModrinthPage::triggerSearch);
- ui->searchEdit->installEventFilter(this);
- listModel = new Modrinth::ListModel(this);
- ui->packView->setModel(listModel);
-
- ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(
- Qt::ScrollBarAsNeeded);
- ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300);
-
- // index is used to set the sorting with the modrinth api
- ui->sortByBox->addItem(tr("Sort by Relevance"));
- ui->sortByBox->addItem(tr("Sort by Downloads"));
- ui->sortByBox->addItem(tr("Sort by Follows"));
- ui->sortByBox->addItem(tr("Sort by last updated"));
- ui->sortByBox->addItem(tr("Sort by newest"));
-
- connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this,
- SLOT(triggerSearch()));
- connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged,
- this, &ModrinthPage::onSelectionChanged);
- connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this,
- &ModrinthPage::onVersionSelectionChanged);
- connect(ui->modSelectionButton, &QPushButton::clicked, this,
- &ModrinthPage::onModSelected);
-}
-
-ModrinthPage::~ModrinthPage() { delete ui; }
-
-bool ModrinthPage::eventFilter(QObject *watched, QEvent *event) {
- if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) {
- QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
- if (keyEvent->key() == Qt::Key_Return) {
- triggerSearch();
- keyEvent->accept();
- return true;
- }
- }
- return QWidget::eventFilter(watched, event);
-}
-
-bool ModrinthPage::shouldDisplay() const { return true; }
-
-void ModrinthPage::retranslate() {
- ui->retranslateUi(this);
-}
-
-void ModrinthPage::openedImpl() {
- updateSelectionButton();
- triggerSearch();
-}
-
-void ModrinthPage::triggerSearch() {
- listModel->searchWithTerm(ui->searchEdit->text(),
- ui->sortByBox->currentIndex());
-}
-
-void ModrinthPage::onSelectionChanged(QModelIndex first, QModelIndex second) {
- ui->versionSelectionBox->clear();
-
- if (!first.isValid()) {
- return;
- }
-
- current = listModel->data(first, Qt::UserRole).value<Modrinth::IndexedPack>();
- QString text = "";
- QString name = current.name;
-
- if (current.websiteUrl.isEmpty())
- text = name;
- else
- text = "<a href=\"" + current.websiteUrl + "\">" + name + "</a>";
- text += "<br>" + tr(" by ") + "<a href=\"" + current.author.url + "\">" +
- current.author.name + "</a><br><br>";
- ui->packDescription->setHtml(text + current.description);
-
- if (!current.versionsLoaded) {
- qDebug() << "Loading Modrinth mod versions";
-
- ui->modSelectionButton->setText(tr("Loading versions..."));
- ui->modSelectionButton->setEnabled(false);
-
- auto netJob =
- new NetJob(QString("Modrinth::ModVersions(%1)").arg(current.name),
- APPLICATION->network());
- auto response = new QByteArray();
- QString addonId = current.addonId;
- netJob->addNetAction(Net::Download::makeByteArray(
- QString("https://api.modrinth.com/v2/project/%1/version").arg(addonId),
- response));
-
- QObject::connect(netJob, &NetJob::succeeded, this, [this, response, addonId] {
- if(addonId != current.addonId){
- return;
- }
- QJsonParseError parse_error;
- QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
- if (parse_error.error != QJsonParseError::NoError) {
- qWarning() << "Error while parsing JSON response from Modrinth at "
- << parse_error.offset
- << " reason: " << parse_error.errorString();
- qWarning() << *response;
- return;
- }
- QJsonArray arr = doc.array();
- try {
- Modrinth::loadIndexedPackVersions(current, arr, APPLICATION->network(),
- m_instance);
- } catch (const JSONValidationError &e) {
- qDebug() << *response;
- qWarning() << "Error while reading Modrinth mod version: " << e.cause();
- }
- auto packProfile = ((MinecraftInstance *)m_instance)->getPackProfile();
- QString mcVersion = packProfile->getComponentVersion("net.minecraft");
- QString loaderString =
- (packProfile->getComponentVersion("net.minecraftforge").isEmpty())
- ? "fabric"
- : "forge";
- for (int i = 0; i < current.versions.size(); i++) {
- auto version = current.versions[i];
- if (!version.mcVersion.contains(mcVersion) ||
- !version.loaders.contains(loaderString)) {
- continue;
- }
- ui->versionSelectionBox->addItem(version.version, QVariant(i));
- }
- if (ui->versionSelectionBox->count() == 0) {
- ui->versionSelectionBox->addItem(tr("No valid version found."),
- QVariant(-1));
- }
-
- ui->modSelectionButton->setText(tr("Cannot select invalid version :("));
- updateSelectionButton();
- });
-
- QObject::connect(netJob, &NetJob::finished, this, [response, netJob] {
- netJob->deleteLater();
- delete response;
- });
-
- netJob->start();
- } else {
- for (int i = 0; i < current.versions.size(); i++) {
- ui->versionSelectionBox->addItem(current.versions[i].version,
- QVariant(i));
- }
- if (ui->versionSelectionBox->count() == 0) {
- ui->versionSelectionBox->addItem(tr("No valid version found."),
- QVariant(-1));
- }
-
- updateSelectionButton();
- }
-}
-
-void ModrinthPage::updateSelectionButton() {
- if (!isOpened || selectedVersion < 0) {
- ui->modSelectionButton->setEnabled(false);
- return;
- }
-
- ui->modSelectionButton->setEnabled(true);
- auto &version = current.versions[selectedVersion];
- if (!dialog->isModSelected(current.name, version.fileName)) {
- ui->modSelectionButton->setText(tr("Select mod for download"));
- } else {
- ui->modSelectionButton->setText(tr("Deselect mod for download"));
- }
+ModrinthPage::ModrinthPage(ModDownloadDialog* dialog, BaseInstance* instance)
+ : ModPage(dialog, instance, new ModrinthAPI())
+{
+ listModel = new Modrinth::ListModel(this);
+ ui->packView->setModel(listModel);
+
+ // index is used to set the sorting with the modrinth api
+ ui->sortByBox->addItem(tr("Sort by Relevence"));
+ ui->sortByBox->addItem(tr("Sort by Downloads"));
+ ui->sortByBox->addItem(tr("Sort by Follows"));
+ ui->sortByBox->addItem(tr("Sort by last updated"));
+ ui->sortByBox->addItem(tr("Sort by newest"));
+
+ // sometimes Qt just ignores virtual slots and doesn't work as intended it seems,
+ // so it's best not to connect them in the parent's contructor...
+ connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch()));
+ connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthPage::onSelectionChanged);
+ connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &ModrinthPage::onVersionSelectionChanged);
+ connect(ui->modSelectionButton, &QPushButton::clicked, this, &ModrinthPage::onModSelected);
}
-void ModrinthPage::onVersionSelectionChanged(QString data) {
- if (data.isNull() || data.isEmpty()) {
- selectedVersion = -1;
- return;
- }
- selectedVersion = ui->versionSelectionBox->currentData().toInt();
- updateSelectionButton();
+auto ModrinthPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, QString loaderVer) const -> bool
+{
+ return ver.mcVersion.contains(mineVer) && ver.loaders.contains(loaderVer);
}
-void ModrinthPage::onModSelected() {
- auto &version = current.versions[selectedVersion];
- if (dialog->isModSelected(current.name, version.fileName)) {
- dialog->removeSelectedMod(current.name);
- } else {
- dialog->addSelectedMod(current.name,
- new ModDownloadTask(version.downloadUrl,
- version.fileName, dialog->mods));
- }
-
- updateSelectionButton();
-}
+// I don't know why, but doing this on the parent class makes it so that
+// other mod providers start loading before being selected, at least with
+// my Qt, so we need to implement this in every derived class...
+auto ModrinthPage::shouldDisplay() const -> bool { return true; }
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h
index 92955d62..aa5ed793 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h
@@ -35,70 +35,26 @@
#pragma once
-#include <QWidget>
+#include "ui/pages/modplatform/ModPage.h"
-#include "ui/pages/BasePage.h"
-#include <Application.h>
-#include "tasks/Task.h"
-#include "modplatform/modrinth/ModrinthPackIndex.h"
+#include "modplatform/modrinth/ModrinthAPI.h"
-namespace Ui
-{
-class ModrinthPage;
-}
-
-class ModDownloadDialog;
-
-namespace Modrinth {
- class ListModel;
-}
-
-class ModrinthPage : public QWidget, public BasePage
-{
+class ModrinthPage : public ModPage {
Q_OBJECT
-public:
- explicit ModrinthPage(ModDownloadDialog *dialog, BaseInstance *instance);
- virtual ~ModrinthPage();
- virtual QString displayName() const override
- {
- return "Modrinth";
- }
- virtual QIcon icon() const override
- {
- return APPLICATION->getThemedIcon("modrinth");
- }
- virtual QString id() const override
- {
- return "modrinth";
- }
- virtual QString helpPage() const override
- {
- return "Modrinth-platform";
- }
- virtual bool shouldDisplay() const override;
- void retranslate() override;
-
- void openedImpl() override;
-
- bool eventFilter(QObject * watched, QEvent * event) override;
-
- BaseInstance *m_instance;
+ public:
+ explicit ModrinthPage(ModDownloadDialog* dialog, BaseInstance* instance);
+ ~ModrinthPage() override = default;
-private:
- void updateSelectionButton();
+ inline auto displayName() const -> QString override { return "Modrinth"; }
+ inline auto icon() const -> QIcon override { return APPLICATION->getThemedIcon("modrinth"); }
+ inline auto id() const -> QString override { return "modrinth"; }
+ inline auto helpPage() const -> QString override { return "Modrinth-platform"; }
-private slots:
- void triggerSearch();
- void onSelectionChanged(QModelIndex first, QModelIndex second);
- void onVersionSelectionChanged(QString data);
- void onModSelected();
+ inline auto debugName() const -> QString override { return "Modrinth"; }
+ inline auto metaEntryBase() const -> QString override { return "ModrinthPacks"; };
-private:
- Ui::ModrinthPage *ui = nullptr;
- ModDownloadDialog* dialog = nullptr;
- Modrinth::ListModel* listModel = nullptr;
- Modrinth::IndexedPack current;
+ auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, QString loaderVer = "") const -> bool override;
- int selectedVersion = -1;
+ auto shouldDisplay() const -> bool override;
};
diff --git a/program_info/LICENSE b/program_info/LICENSE
index 40cc6059..c68207dc 100644
--- a/program_info/LICENSE
+++ b/program_info/LICENSE
@@ -1,4 +1,4 @@
-Attribution-NonCommercial-ShareAlike 4.0 International
+Attribution-ShareAlike 4.0 International
This license only applies to the logos and branding in this folder.
@@ -56,18 +56,18 @@ exhaustive, and do not form part of our licenses.
=======================================================================
-Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International
-Public License
+Creative Commons Attribution-ShareAlike 4.0 International Public
+License
By exercising the Licensed Rights (defined below), You accept and agree
to be bound by the terms and conditions of this Creative Commons
-Attribution-NonCommercial-ShareAlike 4.0 International Public License
-("Public License"). To the extent this Public License may be
-interpreted as a contract, You are granted the Licensed Rights in
-consideration of Your acceptance of these terms and conditions, and the
-Licensor grants You such rights in consideration of benefits the
-Licensor receives from making the Licensed Material available under
-these terms and conditions.
+Attribution-ShareAlike 4.0 International Public License ("Public
+License"). To the extent this Public License may be interpreted as a
+contract, You are granted the Licensed Rights in consideration of Your
+acceptance of these terms and conditions, and the Licensor grants You
+such rights in consideration of benefits the Licensor receives from
+making the Licensed Material available under these terms and
+conditions.
Section 1 -- Definitions.
@@ -86,7 +86,7 @@ Section 1 -- Definitions.
and Similar Rights in Your contributions to Adapted Material in
accordance with the terms and conditions of this Public License.
- c. BY-NC-SA Compatible License means a license listed at
+ c. BY-SA Compatible License means a license listed at
creativecommons.org/compatiblelicenses, approved by Creative
Commons as essentially the equivalent of this Public License.
@@ -110,7 +110,7 @@ Section 1 -- Definitions.
g. License Elements means the license attributes listed in the name
of a Creative Commons Public License. The License Elements of this
- Public License are Attribution, NonCommercial, and ShareAlike.
+ Public License are Attribution and ShareAlike.
h. Licensed Material means the artistic or literary work, database,
or other material to which the Licensor applied this Public
@@ -124,15 +124,7 @@ Section 1 -- Definitions.
j. Licensor means the individual(s) or entity(ies) granting rights
under this Public License.
- k. NonCommercial means not primarily intended for or directed towards
- commercial advantage or monetary compensation. For purposes of
- this Public License, the exchange of the Licensed Material for
- other material subject to Copyright and Similar Rights by digital
- file-sharing or similar means is NonCommercial provided there is
- no payment of monetary compensation in connection with the
- exchange.
-
- l. Share means to provide material to the public by any means or
+ k. Share means to provide material to the public by any means or
process that requires permission under the Licensed Rights, such
as reproduction, public display, public performance, distribution,
dissemination, communication, or importation, and to make material
@@ -140,13 +132,13 @@ Section 1 -- Definitions.
public may access the material from a place and at a time
individually chosen by them.
- m. Sui Generis Database Rights means rights other than copyright
+ l. Sui Generis Database Rights means rights other than copyright
resulting from Directive 96/9/EC of the European Parliament and of
the Council of 11 March 1996 on the legal protection of databases,
as amended and/or succeeded, as well as other essentially
equivalent rights anywhere in the world.
- n. You means the individual or entity exercising the Licensed Rights
+ m. You means the individual or entity exercising the Licensed Rights
under this Public License. Your has a corresponding meaning.
@@ -160,10 +152,9 @@ Section 2 -- Scope.
exercise the Licensed Rights in the Licensed Material to:
a. reproduce and Share the Licensed Material, in whole or
- in part, for NonCommercial purposes only; and
+ in part; and
- b. produce, reproduce, and Share Adapted Material for
- NonCommercial purposes only.
+ b. produce, reproduce, and Share Adapted Material.
2. Exceptions and Limitations. For the avoidance of doubt, where
Exceptions and Limitations apply to Your use, this Public
@@ -231,9 +222,7 @@ Section 2 -- Scope.
Rights, whether directly or through a collecting society
under any voluntary or waivable statutory or compulsory
licensing scheme. In all other cases the Licensor expressly
- reserves any right to collect such royalties, including when
- the Licensed Material is used other than for NonCommercial
- purposes.
+ reserves any right to collect such royalties.
Section 3 -- License Conditions.
@@ -278,6 +267,7 @@ following conditions.
reasonable to satisfy the conditions by providing a URI or
hyperlink to a resource that includes the required
information.
+
3. If requested by the Licensor, You must remove any of the
information required by Section 3(a)(1)(A) to the extent
reasonably practicable.
@@ -289,7 +279,7 @@ following conditions.
1. The Adapter's License You apply must be a Creative Commons
license with the same License Elements, this version or
- later, or a BY-NC-SA Compatible License.
+ later, or a BY-SA Compatible License.
2. You must include the text of, or the URI or hyperlink to, the
Adapter's License You apply. You may satisfy this condition
@@ -309,15 +299,14 @@ apply to Your use of the Licensed Material:
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
to extract, reuse, reproduce, and Share all or a substantial
- portion of the contents of the database for NonCommercial purposes
- only;
+ portion of the contents of the database;
b. if You include all or a substantial portion of the database
contents in a database in which You have Sui Generis Database
Rights, then the database in which You have Sui Generis Database
Rights (but not its individual contents) is Adapted Material,
- including for purposes of Section 3(b); and
+ including for purposes of Section 3(b); and
c. You must comply with the conditions in Section 3(a) if You Share
all or a substantial portion of the contents of the database.
@@ -417,6 +406,7 @@ Section 8 -- Interpretation.
that apply to the Licensor or You, including from the legal
processes of any jurisdiction or authority.
+
=======================================================================
Creative Commons is not a party to its public
@@ -437,3 +427,4 @@ the avoidance of doubt, this paragraph does not form part of the
public licenses.
Creative Commons may be contacted at creativecommons.org.
+