diff options
67 files changed, 1787 insertions, 2110 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 new file mode 100644 index 00000000..fa287a2c --- /dev/null +++ b/.github/workflows/backport.yml @@ -0,0 +1,19 @@ +name: Backport PR to stable +on: + pull_request: + branches: [ develop ] + types: [ closed ] +jobs: + release_pull_request: + if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'backport') + runs-on: ubuntu-latest + steps: + - name: checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Backport PR by cherry-pick-ing + uses: Nathanmalnoury/gh-backport-action@master + with: + pr_branch: 'stable' + github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3e8681c9..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 @@ -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,18 +234,31 @@ 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 + 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@v3 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 @@ -42,3 +42,7 @@ run/ # Nix/NixOS result/ + +# Flatpak +.flatpak-builder +flatbuild diff --git a/CMakeLists.txt b/CMakeLists.txt index b41dd076..b97635c1 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) @@ -59,14 +59,11 @@ set(Launcher_VERSION_HOTFIX 1) set(Launcher_VERSION_BUILD -1 CACHE STRING "Build number. -1 for no build number.") # Build platform. -set(Launcher_BUILD_PLATFORM "" CACHE STRING "A short string identifying the platform that this build was built for. Only used by the notification system and to display in the about dialog.") +set(Launcher_BUILD_PLATFORM "" CACHE STRING "A short string identifying the platform that this build was built for. Only used to display in the about dialog.") # Channel list URL set(Launcher_UPDATER_BASE "" CACHE STRING "Base URL for the updater.") -# Notification URL -set(Launcher_NOTIFICATION_URL "" CACHE STRING "URL for checking for notifications.") - # The metadata server set(Launcher_META_URL "https://meta.polymc.org/v1/" CACHE STRING "URL to fetch Launcher's meta files from.") @@ -48,7 +48,7 @@ If you want to contribute to PolyMC you might find it useful to join our Discord ## Building -If you want to build PolyMC yourself, check [BUILD.md](BUILD.md) for build instructions. +If you want to build PolyMC yourself, check [Build Instructions](https://polymc.org/wiki/development/build-instructions/) for build instructions. ## Code formatting @@ -66,9 +66,15 @@ In general, in order of importance: The translation effort for PolyMC is hosted on [Weblate](https://hosted.weblate.org/projects/polymc/polymc/) and information about translating PolyMC is available at https://github.com/PolyMC/Translations -## Download infomation -To modify download infomation or change packaging infomation send a pull request or issue to the website [Here](https://github.com/PolyMC/polymc.github.io/blob/master/src/download.md) +## Download information +To modify download information or change packaging information send a pull request or issue to the website [Here](https://github.com/PolyMC/polymc.github.io/blob/master/src/download.md) ## Forking/Redistributing/Custom builds policy Do whatever you want, we don't care. Just follow the license. If you have any questions about this feel free to ask in an issue. + +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. diff --git a/buildconfig/BuildConfig.cpp.in b/buildconfig/BuildConfig.cpp.in index 8df627b0..7360d964 100644 --- a/buildconfig/BuildConfig.cpp.in +++ b/buildconfig/BuildConfig.cpp.in @@ -60,8 +60,6 @@ Config::Config() BUILD_PLATFORM = "@Launcher_BUILD_PLATFORM@"; UPDATER_BASE = "@Launcher_UPDATER_BASE@"; - NOTIFICATION_URL = "@Launcher_NOTIFICATION_URL@"; - FULL_VERSION_STR = "@Launcher_VERSION_MAJOR@.@Launcher_VERSION_MINOR@.@Launcher_VERSION_BUILD@"; GIT_COMMIT = "@Launcher_GIT_COMMIT@"; GIT_REFSPEC = "@Launcher_GIT_REFSPEC@"; diff --git a/buildconfig/BuildConfig.h b/buildconfig/BuildConfig.h index 121cfc8f..2fb71f14 100644 --- a/buildconfig/BuildConfig.h +++ b/buildconfig/BuildConfig.h @@ -81,13 +81,6 @@ public: /// User-Agent to use for uncached requests. QString USER_AGENT_UNCACHED; - - /// URL for notifications - QString NOTIFICATION_URL; - - /// Used for matching notifications - QString FULL_VERSION_STR; - /// The git commit hash of this build QString GIT_COMMIT; 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 cbcacb5e..91b7b82c 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -610,9 +610,6 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) m_settings->registerSetting("IconTheme", QString("pe_colored")); m_settings->registerSetting("ApplicationTheme", QString("system")); - // Notifications - m_settings->registerSetting("ShownNotifications", QString()); - // Remembered state m_settings->registerSetting("LastUsedGroupForNewInstance", QString()); @@ -730,6 +727,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", ""); @@ -1142,6 +1140,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(); @@ -1532,7 +1539,7 @@ QString Application::getJarsPath() return FS::PathCombine(m_rootPath, m_jarsPath); } -QString Application::getMSAClientID() +QString Application::getMSAClientID() { QString clientIDOverride = m_settings->get("MSAClientIDOverride").toString(); if (!clientIDOverride.isEmpty()) { 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 6457ae74..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 @@ -174,13 +176,6 @@ add_unit_test(DownloadTask DATA updater/testdata ) -# Rarely used notifications -set(NOTIFICATIONS_SOURCES - # Notifications - short warning messages - notifications/NotificationChecker.h - notifications/NotificationChecker.cpp -) - # Backend for the news bar... there's usually no news. set(NEWS_SOURCES # News System @@ -360,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 @@ -492,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 @@ -561,7 +568,6 @@ set(LOGIC_SOURCES ${NET_SOURCES} ${LAUNCH_SOURCES} ${UPDATE_SOURCES} - ${NOTIFICATIONS_SOURCES} ${NEWS_SOURCES} ${MINECRAFT_SOURCES} ${SCREENSHOTS_SOURCES} @@ -572,6 +578,7 @@ set(LOGIC_SOURCES ${TOOLS_SOURCES} ${META_SOURCES} ${ICONS_SOURCES} + ${API_SOURCES} ${FTB_SOURCES} ${FLAME_SOURCES} ${MODRINTH_SOURCES} @@ -721,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 @@ -791,8 +803,6 @@ SET(LAUNCHER_SOURCES ui/dialogs/NewComponentDialog.h ui/dialogs/NewInstanceDialog.cpp ui/dialogs/NewInstanceDialog.h - ui/dialogs/NotificationDialog.cpp - ui/dialogs/NotificationDialog.h ui/pagedialog/PageDialog.cpp ui/pagedialog/PageDialog.h ui/dialogs/ProgressDialog.cpp @@ -881,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 @@ -895,7 +904,6 @@ qt5_wrap_ui(LAUNCHER_UI ui/dialogs/ProfileSetupDialog.ui ui/dialogs/ProgressDialog.ui ui/dialogs/NewInstanceDialog.ui - ui/dialogs/NotificationDialog.ui ui/dialogs/UpdateDialog.ui ui/dialogs/NewComponentDialog.ui ui/dialogs/ProfileSelectDialog.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 6db12c42..c7e60fda 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -1,4 +1,40 @@ +// 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/>. + * + * 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 "MinecraftInstance.h" +#include "BuildConfig.h" #include "minecraft/launch/CreateGameFolders.h" #include "minecraft/launch/ExtractNatives.h" #include "minecraft/launch/PrintInstanceInfo.h" @@ -20,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" @@ -434,7 +471,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(); @@ -935,6 +972,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/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 992d6657..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"); @@ -60,17 +60,6 @@ void Modrinth::loadIndexedPackVersions(Modrinth::IndexedPack & pack, QJsonArray 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,18 +67,16 @@ void Modrinth::loadIndexedPackVersions(Modrinth::IndexedPack & pack, QJsonArray i++; } - 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/notifications/NotificationChecker.cpp b/launcher/notifications/NotificationChecker.cpp deleted file mode 100644 index 10b91691..00000000 --- a/launcher/notifications/NotificationChecker.cpp +++ /dev/null @@ -1,129 +0,0 @@ -#include "NotificationChecker.h" - -#include <QJsonDocument> -#include <QJsonObject> -#include <QJsonArray> -#include <QDebug> - -#include "net/Download.h" - -#include "Application.h" - -NotificationChecker::NotificationChecker(QObject *parent) - : QObject(parent) -{ -} - -void NotificationChecker::setNotificationsUrl(const QUrl ¬ificationsUrl) -{ - m_notificationsUrl = notificationsUrl; -} - -void NotificationChecker::setApplicationChannel(QString channel) -{ - m_appVersionChannel = channel; -} - -void NotificationChecker::setApplicationFullVersion(QString version) -{ - m_appFullVersion = version; -} - -void NotificationChecker::setApplicationPlatform(QString platform) -{ - m_appPlatform = platform; -} - -QList<NotificationChecker::NotificationEntry> NotificationChecker::notificationEntries() const -{ - return m_entries; -} - -void NotificationChecker::checkForNotifications() -{ - if (!m_notificationsUrl.isValid()) - { - qCritical() << "Failed to check for notifications. No notifications URL set." - << "If you'd like to use PolyMC's notification system, please pass the " - "URL to CMake at compile time."; - return; - } - if (m_checkJob) - { - return; - } - m_checkJob = new NetJob("Checking for notifications", APPLICATION->network()); - auto entry = APPLICATION->metacache()->resolveEntry("root", "notifications.json"); - entry->setStale(true); - m_checkJob->addNetAction(m_download = Net::Download::makeCached(m_notificationsUrl, entry)); - connect(m_download.get(), &Net::Download::succeeded, this, &NotificationChecker::downloadSucceeded); - m_checkJob->start(); -} - -void NotificationChecker::downloadSucceeded(int) -{ - m_entries.clear(); - - QFile file(m_download->getTargetFilepath()); - if (file.open(QFile::ReadOnly)) - { - QJsonArray root = QJsonDocument::fromJson(file.readAll()).array(); - for (auto it = root.begin(); it != root.end(); ++it) - { - QJsonObject obj = (*it).toObject(); - NotificationEntry entry; - entry.id = obj.value("id").toDouble(); - entry.message = obj.value("message").toString(); - entry.channel = obj.value("channel").toString(); - entry.platform = obj.value("platform").toString(); - entry.from = obj.value("from").toString(); - entry.to = obj.value("to").toString(); - const QString type = obj.value("type").toString("critical"); - if (type == "critical") - { - entry.type = NotificationEntry::Critical; - } - else if (type == "warning") - { - entry.type = NotificationEntry::Warning; - } - else if (type == "information") - { - entry.type = NotificationEntry::Information; - } - if(entryApplies(entry)) - m_entries.append(entry); - } - } - - m_checkJob.reset(); - - emit notificationCheckFinished(); -} - -bool versionLessThan(const QString &v1, const QString &v2) -{ - QStringList l1 = v1.split('.'); - QStringList l2 = v2.split('.'); - while (!l1.isEmpty() && !l2.isEmpty()) - { - int one = l1.isEmpty() ? 0 : l1.takeFirst().toInt(); - int two = l2.isEmpty() ? 0 : l2.takeFirst().toInt(); - if (one != two) - { - return one < two; - } - } - return false; -} - -bool NotificationChecker::entryApplies(const NotificationChecker::NotificationEntry& entry) const -{ - bool channelApplies = entry.channel.isEmpty() || entry.channel == m_appVersionChannel; - bool platformApplies = entry.platform.isEmpty() || entry.platform == m_appPlatform; - bool fromApplies = - entry.from.isEmpty() || entry.from == m_appFullVersion || !versionLessThan(m_appFullVersion, entry.from); - bool toApplies = - entry.to.isEmpty() || entry.to == m_appFullVersion || !versionLessThan(entry.to, m_appFullVersion); - return channelApplies && platformApplies && fromApplies && toApplies; -} diff --git a/launcher/notifications/NotificationChecker.h b/launcher/notifications/NotificationChecker.h deleted file mode 100644 index 0f305f33..00000000 --- a/launcher/notifications/NotificationChecker.h +++ /dev/null @@ -1,61 +0,0 @@ -#pragma once - -#include <QObject> - -#include "net/NetJob.h" -#include "net/Download.h" - -class NotificationChecker : public QObject -{ - Q_OBJECT - -public: - explicit NotificationChecker(QObject *parent = 0); - - void setNotificationsUrl(const QUrl ¬ificationsUrl); - void setApplicationPlatform(QString platform); - void setApplicationChannel(QString channel); - void setApplicationFullVersion(QString version); - - struct NotificationEntry - { - int id; - QString message; - enum - { - Critical, - Warning, - Information - } type; - QString channel; - QString platform; - QString from; - QString to; - }; - - QList<NotificationEntry> notificationEntries() const; - -public -slots: - void checkForNotifications(); - -private -slots: - void downloadSucceeded(int); - -signals: - void notificationCheckFinished(); - -private: - bool entryApplies(const NotificationEntry &entry) const; - -private: - QList<NotificationEntry> m_entries; - QUrl m_notificationsUrl; - NetJob::Ptr m_checkJob; - Net::Download::Ptr m_download; - - QString m_appVersionChannel; - QString m_appPlatform; - QString m_appFullVersion; -}; 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/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 4be509b4..47c469e9 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -59,7 +59,6 @@ #include <net/NetJob.h> #include <net/Download.h> #include <news/NewsChecker.h> -#include <notifications/NotificationChecker.h> #include <tools/BaseProfiler.h> #include <updater/DownloadTask.h> #include <updater/UpdateChecker.h> @@ -82,7 +81,6 @@ #include "ui/dialogs/CopyInstanceDialog.h" #include "ui/dialogs/UpdateDialog.h" #include "ui/dialogs/EditAccountDialog.h" -#include "ui/dialogs/NotificationDialog.h" #include "ui/dialogs/ExportInstanceDialog.h" #include "UpdateController.h" @@ -275,8 +273,8 @@ public: { mainToolBar = TranslatedToolbar(MainWindow); mainToolBar->setObjectName(QStringLiteral("mainToolBar")); - mainToolBar->setMovable(false); - mainToolBar->setAllowedAreas(Qt::TopToolBarArea); + mainToolBar->setMovable(true); + mainToolBar->setAllowedAreas(Qt::TopToolBarArea | Qt::BottomToolBarArea); mainToolBar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); mainToolBar->setFloatable(false); mainToolBar.setWindowTitleId(QT_TRANSLATE_NOOP("MainWindow", "Main Toolbar")); @@ -444,8 +442,8 @@ public: { newsToolBar = TranslatedToolbar(MainWindow); newsToolBar->setObjectName(QStringLiteral("newsToolBar")); - newsToolBar->setMovable(false); - newsToolBar->setAllowedAreas(Qt::BottomToolBarArea); + newsToolBar->setMovable(true); + newsToolBar->setAllowedAreas(Qt::TopToolBarArea | Qt::BottomToolBarArea); newsToolBar->setIconSize(QSize(16, 16)); newsToolBar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); newsToolBar->setFloatable(false); @@ -469,6 +467,7 @@ public: instanceToolBar->setObjectName(QStringLiteral("instanceToolBar")); // disabled until we have an instance selected instanceToolBar->setEnabled(false); + instanceToolBar->setMovable(true); instanceToolBar->setAllowedAreas(Qt::LeftToolBarArea | Qt::RightToolBarArea); instanceToolBar->setToolButtonStyle(Qt::ToolButtonTextOnly); instanceToolBar->setFloatable(false); @@ -846,17 +845,6 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow } } - { - auto checker = new NotificationChecker(); - checker->setNotificationsUrl(QUrl(BuildConfig.NOTIFICATION_URL)); - checker->setApplicationChannel(BuildConfig.VERSION_CHANNEL); - checker->setApplicationPlatform(BuildConfig.BUILD_PLATFORM); - checker->setApplicationFullVersion(BuildConfig.FULL_VERSION_STR); - m_notificationChecker.reset(checker); - connect(m_notificationChecker.get(), &NotificationChecker::notificationCheckFinished, this, &MainWindow::notificationsChanged); - checker->checkForNotifications(); - } - setSelectedInstanceById(APPLICATION->settings()->get("SelectedInstance").toString()); // removing this looks stupid @@ -1268,24 +1256,6 @@ QString intListToString(const QList<int> &list) } return slist.join(','); } -void MainWindow::notificationsChanged() -{ - QList<NotificationChecker::NotificationEntry> entries = m_notificationChecker->notificationEntries(); - QList<int> shownNotifications = stringToIntList(APPLICATION->settings()->get("ShownNotifications").toString()); - for (auto it = entries.begin(); it != entries.end(); ++it) - { - NotificationChecker::NotificationEntry entry = *it; - if (!shownNotifications.contains(entry.id)) - { - NotificationDialog dialog(entry, this); - if (dialog.exec() == NotificationDialog::DontShowAgain) - { - shownNotifications.append(entry.id); - } - } - } - APPLICATION->settings()->set("ShownNotifications", intListToString(shownNotifications)); -} void MainWindow::downloadUpdates(GoUpdate::Status status) { diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h index bdbe81aa..f2852d78 100644 --- a/launcher/ui/MainWindow.h +++ b/launcher/ui/MainWindow.h @@ -28,7 +28,6 @@ class LaunchController; class NewsChecker; -class NotificationChecker; class QToolButton; class InstanceProxyModel; class LabeledToolButton; @@ -168,8 +167,6 @@ private slots: void updateNotAvailable(); - void notificationsChanged(); - void defaultAccountChanged(); void changeActiveAccount(); @@ -215,7 +212,6 @@ private: KonamiCode * secretEventFilter = nullptr; unique_qobject_ptr<NewsChecker> m_newsChecker; - unique_qobject_ptr<NotificationChecker> m_notificationChecker; InstancePtr m_selectedInstance; QString m_currentInstIcon; 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("<<a href='%1'>%2</a>>").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 <<a href='mailto:swurl@swurl.xyz'>swurl@swurl.xyz </a>></p>\n"; - stream << "<p>LennyMcLennington <<a href='mailto:lenny@sneed.church'>lenny@sneed.church</a>></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 <<a href='mailto:forkk@forkk.net'>forkk@forkk.net</a>></p>\n"; - stream << "<p>Petr Mrázek <<a href='mailto:peterix@gmail.com'>peterix@gmail.com</a>></p>\n"; + stream << QString("<p>Petr Mrázek <<a href='mailto:peterix@gmail.com'>peterix@gmail.com</a>></p>\n"); stream << "<p>Sky Welch <<a href='mailto:multimc@bunnies.io'>multimc@bunnies.io</a>></p>\n"; stream << "<p>Jan (02JanDal) <<a href='mailto:02jandal@gmail.com'>02jandal@gmail.com</a>></p>\n"; stream << "<p>RoboSky <<a href='https://twitter.com/RoboSky_'>@RoboSky_</a>></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/dialogs/NotificationDialog.cpp b/launcher/ui/dialogs/NotificationDialog.cpp deleted file mode 100644 index f2a35ae2..00000000 --- a/launcher/ui/dialogs/NotificationDialog.cpp +++ /dev/null @@ -1,86 +0,0 @@ -#include "NotificationDialog.h" -#include "ui_NotificationDialog.h" - -#include <QTimerEvent> -#include <QStyle> - -NotificationDialog::NotificationDialog(const NotificationChecker::NotificationEntry &entry, QWidget *parent) : - QDialog(parent, Qt::MSWindowsFixedSizeDialogHint | Qt::WindowTitleHint | Qt::CustomizeWindowHint), - ui(new Ui::NotificationDialog) -{ - ui->setupUi(this); - - QStyle::StandardPixmap icon; - switch (entry.type) - { - case NotificationChecker::NotificationEntry::Critical: - icon = QStyle::SP_MessageBoxCritical; - break; - case NotificationChecker::NotificationEntry::Warning: - icon = QStyle::SP_MessageBoxWarning; - break; - default: - case NotificationChecker::NotificationEntry::Information: - icon = QStyle::SP_MessageBoxInformation; - break; - } - ui->iconLabel->setPixmap(style()->standardPixmap(icon, 0, this)); - ui->messageLabel->setText(entry.message); - - m_dontShowAgainText = tr("Don't show again"); - m_closeText = tr("Close"); - - ui->dontShowAgainBtn->setText(m_dontShowAgainText + QString(" (%1)").arg(m_dontShowAgainTime)); - ui->closeBtn->setText(m_closeText + QString(" (%1)").arg(m_closeTime)); - - startTimer(1000); -} - -NotificationDialog::~NotificationDialog() -{ - delete ui; -} - -void NotificationDialog::timerEvent(QTimerEvent *event) -{ - if (m_dontShowAgainTime > 0) - { - m_dontShowAgainTime--; - if (m_dontShowAgainTime == 0) - { - ui->dontShowAgainBtn->setText(m_dontShowAgainText); - ui->dontShowAgainBtn->setEnabled(true); - } - else - { - ui->dontShowAgainBtn->setText(m_dontShowAgainText + QString(" (%1)").arg(m_dontShowAgainTime)); - } - } - if (m_closeTime > 0) - { - m_closeTime--; - if (m_closeTime == 0) - { - ui->closeBtn->setText(m_closeText); - ui->closeBtn->setEnabled(true); - } - else - { - ui->closeBtn->setText(m_closeText + QString(" (%1)").arg(m_closeTime)); - } - } - - if (m_closeTime == 0 && m_dontShowAgainTime == 0) - { - killTimer(event->timerId()); - } -} - -void NotificationDialog::on_dontShowAgainBtn_clicked() -{ - done(DontShowAgain); -} -void NotificationDialog::on_closeBtn_clicked() -{ - done(Normal); -} diff --git a/launcher/ui/dialogs/NotificationDialog.h b/launcher/ui/dialogs/NotificationDialog.h deleted file mode 100644 index e1cbb9fa..00000000 --- a/launcher/ui/dialogs/NotificationDialog.h +++ /dev/null @@ -1,44 +0,0 @@ -#ifndef NOTIFICATIONDIALOG_H -#define NOTIFICATIONDIALOG_H - -#include <QDialog> - -#include "notifications/NotificationChecker.h" - -namespace Ui { -class NotificationDialog; -} - -class NotificationDialog : public QDialog -{ - Q_OBJECT - -public: - explicit NotificationDialog(const NotificationChecker::NotificationEntry &entry, QWidget *parent = 0); - ~NotificationDialog(); - - enum ExitCode - { - Normal, - DontShowAgain - }; - -protected: - void timerEvent(QTimerEvent *event); - -private: - Ui::NotificationDialog *ui; - - int m_dontShowAgainTime = 10; - int m_closeTime = 5; - - QString m_dontShowAgainText; - QString m_closeText; - -private -slots: - void on_dontShowAgainBtn_clicked(); - void on_closeBtn_clicked(); -}; - -#endif // NOTIFICATIONDIALOG_H diff --git a/launcher/ui/dialogs/NotificationDialog.ui b/launcher/ui/dialogs/NotificationDialog.ui deleted file mode 100644 index 3e6c22bc..00000000 --- a/launcher/ui/dialogs/NotificationDialog.ui +++ /dev/null @@ -1,85 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<ui version="4.0"> - <class>NotificationDialog</class> - <widget class="QDialog" name="NotificationDialog"> - <property name="geometry"> - <rect> - <x>0</x> - <y>0</y> - <width>320</width> - <height>240</height> - </rect> - </property> - <property name="windowTitle"> - <string>Notification</string> - </property> - <layout class="QVBoxLayout" name="verticalLayout" stretch="1,0"> - <item> - <layout class="QHBoxLayout" name="horizontalLayout_2" stretch="0,1"> - <item> - <widget class="QLabel" name="iconLabel"> - <property name="text"> - <string notr="true">TextLabel</string> - </property> - </widget> - </item> - <item> - <widget class="QLabel" name="messageLabel"> - <property name="text"> - <string notr="true">TextLabel</string> - </property> - <property name="wordWrap"> - <bool>true</bool> - </property> - <property name="openExternalLinks"> - <bool>true</bool> - </property> - <property name="textInteractionFlags"> - <set>Qt::TextBrowserInteraction</set> - </property> - </widget> - </item> - </layout> - </item> - <item> - <layout class="QHBoxLayout" name="horizontalLayout"> - <item> - <spacer name="horizontalSpacer"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>40</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - <item> - <widget class="QPushButton" name="dontShowAgainBtn"> - <property name="enabled"> - <bool>false</bool> - </property> - <property name="text"> - <string>Don't show again</string> - </property> - </widget> - </item> - <item> - <widget class="QPushButton" name="closeBtn"> - <property name="enabled"> - <bool>false</bool> - </property> - <property name="text"> - <string>Close</string> - </property> - </widget> - </item> - </layout> - </item> - </layout> - </widget> - <resources/> - <connections/> -</ui> 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 64e668e9..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); @@ -254,11 +273,6 @@ void LauncherPage::applySettings() { auto s = APPLICATION->settings(); - if (ui->resetNotificationsBtn->isChecked()) - { - s->set("ShownNotifications", QString()); - } - // Updates s->set("AutoUpdate", ui->autoUpdateCheckBox->isChecked()); s->set("UpdateChannel", m_currentUpdateChannel); diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index 59096a28..c110dd09 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -185,25 +185,6 @@ </attribute> <layout class="QVBoxLayout" name="verticalLayout_6"> <item> - <widget class="QGroupBox" name="groupBox_3"> - <property name="title"> - <string>Launcher notifications</string> - </property> - <layout class="QVBoxLayout" name="verticalLayout_5"> - <item> - <widget class="QPushButton" name="resetNotificationsBtn"> - <property name="text"> - <string>Reset hidden notifications</string> - </property> - <property name="checkable"> - <bool>true</bool> - </property> - </widget> - </item> - </layout> - </widget> - </item> - <item> <widget class="QGroupBox" name="sortingModeBox"> <property name="enabled"> <bool>true</bool> @@ -499,7 +480,6 @@ <tabstop>modsDirBrowseBtn</tabstop> <tabstop>iconsDirTextBox</tabstop> <tabstop>iconsDirBrowseBtn</tabstop> - <tabstop>resetNotificationsBtn</tabstop> <tabstop>sortLastLaunchedBtn</tabstop> <tabstop>sortByNameBtn</tabstop> <tabstop>themeComboBox</tabstop> 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><html><head/><body><p>PolyMC will automatically exit if the game crashes or exists.</p></body></html></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/polymc-header-black.svg b/program_info/polymc-header-black.svg index 34cda4ab..e9e7c3e2 100644 --- a/program_info/polymc-header-black.svg +++ b/program_info/polymc-header-black.svg @@ -1,31 +1,23 @@ <?xml version="1.0" encoding="UTF-8"?> -<!-- Created with Inkscape (http://www.inkscape.org/) --> -<svg width="1424" height="512" version="1.1" viewBox="0 0 376.77 135.47" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> +<svg viewBox="0 0 376.77 135.47" xmlns="http://www.w3.org/2000/svg" class="home"> <defs> - <linearGradient id="linearGradient84726" x1="4.4979" x2="12.435" y1="3.8011" y2="9.5681" gradientUnits="userSpaceOnUse"> + <linearGradient id="a" x1="4.498" x2="12.435" y1="3.801" y2="9.568" gradientUnits="userSpaceOnUse"> <stop stop-color="#88b858" offset="0"/> <stop stop-color="#72b147" offset=".5"/> <stop stop-color="#5a9a30" offset="1"/> </linearGradient> </defs> - <g transform="matrix(6.95 0 0 6.9572 3.7759 1.0225)"> - <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="m0.52917 6.6846 7.9375 5.7669 7.9375-5.7669-7.9375-5.7669z" fill="url(#linearGradient84726)"/> + <path d="M42.375 118.668s0-24.797 34.094-24.797c34.103 0 34.094 24.797 34.094 24.797z" fill="#765338"/> + <path d="M76.47 93.872 42.376 118.67 21.304 53.751z" fill="#b7835a"/> + <path d="m76.47 93.872 55.165-40.121-21.072 64.918z" fill="#5b422d"/> + <path d="m79 95.709-2.53 1.84-2.532-1.84c0-1.84 2.531-1.84 2.531-1.84s2.531 0 2.531 1.84z" fill="#72b147"/> + <path d="M76.47 93.872s0-40.121 55.165-40.121l-1.564 4.819-6.384 8.324-6.384.962-6.383 8.324-6.384.961-6.384 8.325-6.384.961-6.384 8.324-6.383.962z" fill="#5a9a30"/> + <path d="m73.938 95.709-6.383-.961-6.384-8.325-6.384-.961-6.384-8.324-6.384-.962-6.383-8.324-6.384-.962-6.384-8.324-1.564-4.819c55.165 0 55.165 40.121 55.165 40.121z" fill="#88b858"/> + <path d="m.53 6.685 7.937 5.766 7.937-5.766L8.467.918z" fill="url(#a)" transform="matrix(6.95 0 0 6.9572 17.626 7.241)"/> + <path d="m22.868 58.567-1.564-4.82L76.469 93.87l55.166-40.122-1.564 4.82L76.47 97.55z" fill="none"/> + <g fill="#000" stroke-width=".265" aria-label="PolyMC"> + <g stroke-width=".01" aria-label="PolyMC"> + <path d="M168.153 47.323q5.434 0 9.287 1.858 3.852 1.788 5.916 5.228 2.133 3.44 2.133 8.324 0 2.958-.895 5.847-.894 2.82-2.889 5.16-1.926 2.27-5.09 3.646t-7.705 1.375h-7.361v18.3h-6.673V47.322zm.688 25.041q2.958 0 4.884-.963 1.927-.963 3.027-2.408 1.1-1.513 1.582-3.164.482-1.651.482-2.958 0-1.514-.482-3.096-.481-1.651-1.65-2.958-1.101-1.376-2.959-2.202-1.788-.894-4.471-.894h-7.705v18.643zM187.49 82.6q0-4.265 1.994-7.705 2.064-3.44 5.641-5.434 3.577-1.995 8.118-1.995 4.678 0 8.186 1.995 3.509 1.995 5.435 5.434 1.926 3.44 1.926 7.705t-1.926 7.773q-1.926 3.44-5.503 5.435-3.509 1.995-8.187 1.995-4.54 0-8.118-1.857-3.508-1.927-5.572-5.298-1.995-3.44-1.995-8.048zm6.397.07q0 2.751 1.238 5.021 1.238 2.202 3.302 3.509 2.133 1.307 4.678 1.307 2.683 0 4.747-1.307 2.133-1.307 3.302-3.509 1.17-2.27 1.17-5.022 0-2.752-1.17-4.953-1.17-2.27-3.302-3.577-2.064-1.376-4.747-1.376-2.614 0-4.747 1.376-2.063 1.376-3.302 3.646-1.17 2.201-1.17 4.884zM223.251 43.858h6.398v53.246h-6.398zM238.914 110.808l18.78-42.17h5.917l-18.368 42.17zm7.017-13.484-13.69-28.687h7.223l11.489 25.453zM266.775 97.085V51.2h.063l23.855 33.774-3.043-.67L311.384 51.2h.122v45.885h-7.06v-29.88l.487 3.59-16.065 22.7h-.122l-16.31-22.7 1.218-3.286v29.576zM352.299 93.62q-.974.67-2.921 1.643-1.887.973-4.564 1.704-2.617.67-5.843.608-5.172-.062-9.31-1.825-4.138-1.826-6.999-4.869-2.86-3.104-4.442-7.06-1.522-4.016-1.522-8.52 0-5.05 1.583-9.25t4.503-7.241q2.921-3.104 6.938-4.808 4.016-1.704 8.763-1.704 4.199 0 7.485 1.156 3.347 1.096 5.598 2.496l-2.799 6.633q-1.704-1.156-4.26-2.313-2.556-1.156-5.781-1.156-2.921 0-5.599 1.217-2.678 1.156-4.686 3.347-2.008 2.13-3.225 4.99-1.156 2.86-1.156 6.208 0 3.468 1.034 6.39 1.096 2.92 3.043 5.05 2.008 2.07 4.747 3.287 2.799 1.156 6.268 1.156 3.408 0 5.964-1.035 2.616-1.095 4.199-2.434z"/> </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"/> - </g> - <g id="polymc-header-text" fill="black" transform="matrix(6.9306 0 0 6.9306 -702.9 -659.02)" stroke-width=".26458" aria-label="PolyMC"> - <path d="m120.51 108.48v2.7957h-1.3074v-7.5241h2.8784q1.2609 0 1.9999 0.65629 0.74414 0.65629 0.74414 1.7363 0 1.1059-0.72864 1.7208-0.72346 0.61495-2.0309 0.61495zm0-1.049h1.571q0.69763 0 1.0645-0.32556 0.3669-0.33073 0.3669-0.95084 0-0.60978-0.37207-0.97152-0.37207-0.3669-1.0232-0.37723h-1.6071z"/> - <path d="m125.55 108.43q0-0.82165 0.32556-1.4779 0.32556-0.66145 0.91467-1.0128 0.58911-0.35657 1.3539-0.35657 1.1317 0 1.8345 0.72864 0.70797 0.72863 0.76481 1.9327l5e-3 0.29455q0 0.82682-0.32039 1.478-0.31523 0.65112-0.9095 1.0077-0.58911 0.35657-1.3643 0.35657-1.1834 0-1.8965-0.78548-0.70796-0.79065-0.70796-2.1032zm1.2557 0.10852q0 0.863 0.35657 1.3539 0.35656 0.48576 0.99218 0.48576t0.98702-0.49609q0.35657-0.49609 0.35657-1.4521 0-0.84749-0.36691-1.3436-0.36173-0.49609-0.98701-0.49609-0.61495 0-0.97668 0.49092-0.36174 0.48576-0.36174 1.4573z"/> - <path d="m133.14 111.27h-1.2557v-7.9375h1.2557z"/> - <path d="m136.46 109.47 1.1369-3.793h1.3384l-2.2221 6.4389q-0.5116 1.4108-1.7363 1.4108-0.27388 0-0.60461-0.093v-0.97151l0.23771 0.0155q0.47542 0 0.71314-0.1757 0.24287-0.17053 0.3824-0.57877l0.18087-0.48059-1.9637-5.5655h1.3539z"/> - <path d="m141.48 103.75 2.1704 5.7671 2.1652-5.7671h1.6898v7.5241h-1.3022v-2.4805l0.12919-3.3176-2.2221 5.7981h-0.93534l-2.2169-5.7929 0.12919 3.3124v2.4805h-1.3022v-7.5241z"/> - <path d="m154.79 108.82q-0.11369 1.2041-0.88883 1.881-0.77515 0.67179-2.0619 0.67179-0.89916 0-1.5865-0.42375-0.68212-0.42891-1.0542-1.2144t-0.38758-1.8242v-0.7028q0-1.0645 0.37724-1.8758 0.37724-0.81131 1.08-1.2506 0.70797-0.43925 1.633-0.43925 1.2454 0 2.005 0.67696 0.75965 0.67696 0.88367 1.912h-1.3022q-0.093-0.81132-0.47543-1.1679-0.37723-0.36174-1.111-0.36174-0.85265 0-1.3126 0.62529-0.45475 0.62011-0.46509 1.8242v0.66662q0 1.2196 0.43408 1.8604 0.43925 0.64078 1.2816 0.64078 0.76998 0 1.1576-0.34623t0.49093-1.1524z"/> </g> </svg> diff --git a/program_info/polymc-header.Source.svg b/program_info/polymc-header.Source.svg new file mode 100644 index 00000000..c960f33b --- /dev/null +++ b/program_info/polymc-header.Source.svg @@ -0,0 +1,139 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + width="1424" + height="512" + version="1.1" + viewBox="0 0 376.77 135.47" + id="svg866" + sodipodi:docname="PolyMC-Header.Source.svg" + inkscape:version="1.1.2 (0a00cf5339, 2022-02-04, custom)" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + <sodipodi:namedview + id="namedview868" + pagecolor="#343434" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageshadow="2" + inkscape:pageopacity="0" + inkscape:pagecheckerboard="0" + showgrid="false" + inkscape:zoom="0.61938202" + inkscape:cx="534.40363" + inkscape:cy="299.49206" + inkscape:window-width="2560" + inkscape:window-height="1382" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="1" + inkscape:current-layer="svg866" /> + <defs + id="defs831"> + <linearGradient + id="linearGradient84726" + x1="4.4979" + x2="12.435" + y1="3.8011" + y2="9.5681" + gradientUnits="userSpaceOnUse"> + <stop + stop-color="#88b858" + offset="0" + id="stop824" /> + <stop + stop-color="#72b147" + offset=".5" + id="stop826" /> + <stop + stop-color="#5a9a30" + offset="1" + id="stop828" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient84726" + id="linearGradient1147" + gradientUnits="userSpaceOnUse" + x1="4.4979" + y1="3.8011" + x2="12.435" + y2="9.5681" /> + </defs> + <g + id="g1157"> + <g + transform="matrix(6.95,0,0,6.9572,17.626477,7.241195)" + id="g1322"> + <g + id="g1318"> + <path + d="m 3.561,16.016 c 0,0 0,-3.5642 4.9056,-3.5642 4.9069,0 4.9056,3.5642 4.9056,3.5642 z" + fill="#765338" + id="path1304" /> + <path + d="M 8.4667,12.452 3.5611,16.0162 0.5292,6.6851 Z" + fill="#b7835a" + id="path1306" /> + <path + d="m 8.4667,12.452 7.9375,-5.7669 -3.0319,9.3311 z" + fill="#5b422d" + id="path1308" /> + <path + d="M 8.8308,12.716 8.46663,12.98058 8.10246,12.716 c 0,-0.26458 0.36417,-0.26458 0.36417,-0.26458 0,0 0.36417,0 0.36417,0.26458 z" + fill="#72b147" + id="path1310" /> + <path + d="m 8.4667,12.452 c 0,0 -2e-7,-5.7669 7.9375,-5.7669 l -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.13819 z" + fill="#5a9a30" + id="path1312" /> + <path + d="M 8.1025,12.716 7.18397,12.57781 6.26544,11.38131 5.34691,11.24312 4.42838,10.04662 3.50985,9.90843 2.59132,8.71193 1.67279,8.57374 0.75426,7.37724 0.52919,6.68455 c 7.9375,1e-7 7.9375,5.7669 7.9375,5.7669 z" + fill="#88b858" + id="path1314" /> + <path + d="m 0.52917,6.6846 7.9375,5.7669 7.9375,-5.7669 -7.9375,-5.7669 z" + fill="url(#linearGradient84726)" + id="path1316" + style="fill:url(#linearGradient1147)" /> + </g> + <path + d="m 0.75424,7.3773 -0.22507,-0.69269 7.9375,5.7669 7.9375,-5.7669 -0.22507,0.69269 -7.7124,5.6034 z" + fill-opacity="0" + id="path1320" /> + </g> + <g + id="polymc-header-text-3" + fill="white" + transform="matrix(6.9306,0,0,6.9306,-695.39957,-649.40511)" + stroke-width="0.26458" + aria-label="PolyMC" + style="fill:#ffffff;fill-opacity:1"> + <text + xml:space="preserve" + style="font-size:9.92603px;line-height:1.25;font-family:sans-serif;letter-spacing:-0.610833px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke-width:0.0101009" + x="121.65298" + y="107.71044" + id="text4534"><tspan + sodipodi:role="line" + id="tspan4532" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:9.92603px;font-family:'Josefin Sans';-inkscape-font-specification:'Josefin Sans';fill:#ffffff;fill-opacity:1;stroke-width:0.0101009" + x="121.65298" + y="107.71044">Poly<tspan + style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:8.78072px;font-family:'Josefin Sans';-inkscape-font-specification:'Josefin Sans Semi-Bold'" + id="tspan137828" + rotate="0 0">MC</tspan></tspan></text> + </g> + </g> + <g + id="polymc-header-text-4" + fill="white" + transform="matrix(6.9306,0,0,6.9306,-697.30938,-585.54339)" + stroke-width="0.26458" + aria-label="PolyMC" + style="fill:#ffffff;fill-opacity:1" /> +</svg> diff --git a/program_info/polymc-header.svg b/program_info/polymc-header.svg index a896787f..837004e1 100644 --- a/program_info/polymc-header.svg +++ b/program_info/polymc-header.svg @@ -1,31 +1,23 @@ <?xml version="1.0" encoding="UTF-8"?> -<!-- Created with Inkscape (http://www.inkscape.org/) --> -<svg width="1424" height="512" version="1.1" viewBox="0 0 376.77 135.47" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> +<svg viewBox="0 0 376.77 135.47" xmlns="http://www.w3.org/2000/svg" class="home"> <defs> - <linearGradient id="linearGradient84726" x1="4.4979" x2="12.435" y1="3.8011" y2="9.5681" gradientUnits="userSpaceOnUse"> + <linearGradient id="a" x1="4.498" x2="12.435" y1="3.801" y2="9.568" gradientUnits="userSpaceOnUse"> <stop stop-color="#88b858" offset="0"/> <stop stop-color="#72b147" offset=".5"/> <stop stop-color="#5a9a30" offset="1"/> </linearGradient> </defs> - <g transform="matrix(6.95 0 0 6.9572 3.7759 1.0225)"> - <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="m0.52917 6.6846 7.9375 5.7669 7.9375-5.7669-7.9375-5.7669z" fill="url(#linearGradient84726)"/> + <path d="M42.375 118.668s0-24.797 34.094-24.797c34.103 0 34.094 24.797 34.094 24.797z" fill="#765338"/> + <path d="M76.47 93.872 42.376 118.67 21.304 53.751z" fill="#b7835a"/> + <path d="m76.47 93.872 55.165-40.121-21.072 64.918z" fill="#5b422d"/> + <path d="m79 95.709-2.53 1.84-2.532-1.84c0-1.84 2.531-1.84 2.531-1.84s2.531 0 2.531 1.84z" fill="#72b147"/> + <path d="M76.47 93.872s0-40.121 55.165-40.121l-1.564 4.819-6.384 8.324-6.384.962-6.383 8.324-6.384.961-6.384 8.325-6.384.961-6.384 8.324-6.383.962z" fill="#5a9a30"/> + <path d="m73.938 95.709-6.383-.961-6.384-8.325-6.384-.961-6.384-8.324-6.384-.962-6.383-8.324-6.384-.962-6.384-8.324-1.564-4.819c55.165 0 55.165 40.121 55.165 40.121z" fill="#88b858"/> + <path d="m.53 6.685 7.937 5.766 7.937-5.766L8.467.918z" fill="url(#a)" transform="matrix(6.95 0 0 6.9572 17.626 7.241)"/> + <path d="m22.868 58.567-1.564-4.82L76.469 93.87l55.166-40.122-1.564 4.82L76.47 97.55z" fill="none"/> + <g fill="#fff" stroke-width=".265" aria-label="PolyMC"> + <g stroke-width=".01" aria-label="PolyMC"> + <path d="M168.153 47.323q5.434 0 9.287 1.858 3.852 1.788 5.916 5.228 2.133 3.44 2.133 8.324 0 2.958-.895 5.847-.894 2.82-2.889 5.16-1.926 2.27-5.09 3.646t-7.705 1.375h-7.361v18.3h-6.673V47.322zm.688 25.041q2.958 0 4.884-.963 1.927-.963 3.027-2.408 1.1-1.513 1.582-3.164.482-1.651.482-2.958 0-1.514-.482-3.096-.481-1.651-1.65-2.958-1.101-1.376-2.959-2.202-1.788-.894-4.471-.894h-7.705v18.643zM187.49 82.6q0-4.265 1.994-7.705 2.064-3.44 5.641-5.434 3.577-1.995 8.118-1.995 4.678 0 8.186 1.995 3.509 1.995 5.435 5.434 1.926 3.44 1.926 7.705t-1.926 7.773q-1.926 3.44-5.503 5.435-3.509 1.995-8.187 1.995-4.54 0-8.118-1.857-3.508-1.927-5.572-5.298-1.995-3.44-1.995-8.048zm6.397.07q0 2.751 1.238 5.021 1.238 2.202 3.302 3.509 2.133 1.307 4.678 1.307 2.683 0 4.747-1.307 2.133-1.307 3.302-3.509 1.17-2.27 1.17-5.022 0-2.752-1.17-4.953-1.17-2.27-3.302-3.577-2.064-1.376-4.747-1.376-2.614 0-4.747 1.376-2.063 1.376-3.302 3.646-1.17 2.201-1.17 4.884zM223.251 43.858h6.398v53.246h-6.398zM238.914 110.808l18.78-42.17h5.917l-18.368 42.17zm7.017-13.484-13.69-28.687h7.223l11.489 25.453zM266.775 97.085V51.2h.063l23.855 33.774-3.043-.67L311.384 51.2h.122v45.885h-7.06v-29.88l.487 3.59-16.065 22.7h-.122l-16.31-22.7 1.218-3.286v29.576zM352.299 93.62q-.974.67-2.921 1.643-1.887.973-4.564 1.704-2.617.67-5.843.608-5.172-.062-9.31-1.825-4.138-1.826-6.999-4.869-2.86-3.104-4.442-7.06-1.522-4.016-1.522-8.52 0-5.05 1.583-9.25t4.503-7.241q2.921-3.104 6.938-4.808 4.016-1.704 8.763-1.704 4.199 0 7.485 1.156 3.347 1.096 5.598 2.496l-2.799 6.633q-1.704-1.156-4.26-2.313-2.556-1.156-5.781-1.156-2.921 0-5.599 1.217-2.678 1.156-4.686 3.347-2.008 2.13-3.225 4.99-1.156 2.86-1.156 6.208 0 3.468 1.034 6.39 1.096 2.92 3.043 5.05 2.008 2.07 4.747 3.287 2.799 1.156 6.268 1.156 3.408 0 5.964-1.035 2.616-1.095 4.199-2.434z"/> </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"/> - </g> - <g id="polymc-header-text" fill="white" transform="matrix(6.9306 0 0 6.9306 -702.9 -659.02)" stroke-width=".26458" aria-label="PolyMC"> - <path d="m120.51 108.48v2.7957h-1.3074v-7.5241h2.8784q1.2609 0 1.9999 0.65629 0.74414 0.65629 0.74414 1.7363 0 1.1059-0.72864 1.7208-0.72346 0.61495-2.0309 0.61495zm0-1.049h1.571q0.69763 0 1.0645-0.32556 0.3669-0.33073 0.3669-0.95084 0-0.60978-0.37207-0.97152-0.37207-0.3669-1.0232-0.37723h-1.6071z"/> - <path d="m125.55 108.43q0-0.82165 0.32556-1.4779 0.32556-0.66145 0.91467-1.0128 0.58911-0.35657 1.3539-0.35657 1.1317 0 1.8345 0.72864 0.70797 0.72863 0.76481 1.9327l5e-3 0.29455q0 0.82682-0.32039 1.478-0.31523 0.65112-0.9095 1.0077-0.58911 0.35657-1.3643 0.35657-1.1834 0-1.8965-0.78548-0.70796-0.79065-0.70796-2.1032zm1.2557 0.10852q0 0.863 0.35657 1.3539 0.35656 0.48576 0.99218 0.48576t0.98702-0.49609q0.35657-0.49609 0.35657-1.4521 0-0.84749-0.36691-1.3436-0.36173-0.49609-0.98701-0.49609-0.61495 0-0.97668 0.49092-0.36174 0.48576-0.36174 1.4573z"/> - <path d="m133.14 111.27h-1.2557v-7.9375h1.2557z"/> - <path d="m136.46 109.47 1.1369-3.793h1.3384l-2.2221 6.4389q-0.5116 1.4108-1.7363 1.4108-0.27388 0-0.60461-0.093v-0.97151l0.23771 0.0155q0.47542 0 0.71314-0.1757 0.24287-0.17053 0.3824-0.57877l0.18087-0.48059-1.9637-5.5655h1.3539z"/> - <path d="m141.48 103.75 2.1704 5.7671 2.1652-5.7671h1.6898v7.5241h-1.3022v-2.4805l0.12919-3.3176-2.2221 5.7981h-0.93534l-2.2169-5.7929 0.12919 3.3124v2.4805h-1.3022v-7.5241z"/> - <path d="m154.79 108.82q-0.11369 1.2041-0.88883 1.881-0.77515 0.67179-2.0619 0.67179-0.89916 0-1.5865-0.42375-0.68212-0.42891-1.0542-1.2144t-0.38758-1.8242v-0.7028q0-1.0645 0.37724-1.8758 0.37724-0.81131 1.08-1.2506 0.70797-0.43925 1.633-0.43925 1.2454 0 2.005 0.67696 0.75965 0.67696 0.88367 1.912h-1.3022q-0.093-0.81132-0.47543-1.1679-0.37723-0.36174-1.111-0.36174-0.85265 0-1.3126 0.62529-0.45475 0.62011-0.46509 1.8242v0.66662q0 1.2196 0.43408 1.8604 0.43925 0.64078 1.2816 0.64078 0.76998 0 1.1576-0.34623t0.49093-1.1524z"/> </g> </svg> |