├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── build.yml │ ├── publish.yml │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── Readme.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── screenshots ├── categories.png ├── mod-overview.png ├── mod-update.png ├── search.png └── updatable-mods.png ├── settings.gradle └── src ├── main ├── java │ └── xyz │ │ └── deathsgun │ │ └── modmanager │ │ ├── ModMenuEntrypoint.java │ │ └── mixin │ │ └── ModsScreenMixin.java ├── kotlin │ └── xyz │ │ └── deathsgun │ │ └── modmanager │ │ ├── ModManager.kt │ │ ├── PreLaunchHook.kt │ │ ├── api │ │ ├── ModInstallResult.kt │ │ ├── ModRemoveResult.kt │ │ ├── ModUpdateResult.kt │ │ ├── gui │ │ │ └── list │ │ │ │ ├── IListScreen.kt │ │ │ │ ├── ListWidget.kt │ │ │ │ └── MultiSelectListWidget.kt │ │ ├── http │ │ │ ├── CategoriesResult.kt │ │ │ ├── HttpClient.kt │ │ │ ├── ModResult.kt │ │ │ ├── ModsResult.kt │ │ │ └── VersionResult.kt │ │ ├── mod │ │ │ ├── Asset.kt │ │ │ ├── Category.kt │ │ │ ├── Mod.kt │ │ │ ├── State.kt │ │ │ ├── Version.kt │ │ │ └── VersionType.kt │ │ └── provider │ │ │ ├── IModProvider.kt │ │ │ ├── IModUpdateProvider.kt │ │ │ └── Sorting.kt │ │ ├── config │ │ └── Config.kt │ │ ├── gui │ │ ├── ConfigScreen.kt │ │ ├── ErrorScreen.kt │ │ ├── ModDetailScreen.kt │ │ ├── ModProgressScreen.kt │ │ ├── ModUpdateInfoScreen.kt │ │ ├── ModsOverviewScreen.kt │ │ ├── UpdateAllScreen.kt │ │ └── widget │ │ │ ├── CategoryListEntry.kt │ │ │ ├── CategoryListWidget.kt │ │ │ ├── DescriptionWidget.kt │ │ │ ├── ModListEntry.kt │ │ │ ├── ModListWidget.kt │ │ │ ├── TexturedButton.kt │ │ │ ├── UpdateProgressListEntry.kt │ │ │ └── UpdateProgressListWidget.kt │ │ ├── icon │ │ └── IconCache.kt │ │ ├── md │ │ └── Markdown.kt │ │ ├── models │ │ └── FabricMetadata.kt │ │ ├── providers │ │ └── modrinth │ │ │ ├── Modrinth.kt │ │ │ └── models │ │ │ ├── DetailedMod.kt │ │ │ ├── ModResult.kt │ │ │ ├── ModrinthVersion.kt │ │ │ └── SearchResult.kt │ │ ├── state │ │ └── SavedState.kt │ │ └── update │ │ ├── ProgressListener.kt │ │ ├── Update.kt │ │ ├── UpdateManager.kt │ │ └── VersionFinder.kt └── resources │ ├── assets │ └── modmanager │ │ ├── icon.png │ │ ├── lang │ │ ├── en_us.json │ │ ├── es_ar.json │ │ ├── es_cl.json │ │ ├── es_ec.json │ │ ├── es_es.json │ │ ├── es_mx.json │ │ ├── es_uy.json │ │ ├── es_ve.json │ │ ├── ko_kr.json │ │ ├── ru_ru.json │ │ ├── tr_tr.json │ │ └── zh_cn.json │ │ └── textures │ │ └── gui │ │ ├── hide_button.png │ │ ├── install_button.png │ │ ├── loading.png │ │ └── show_button.png │ ├── build.info │ ├── fabric.mod.json │ └── modmanager.mixins.json └── test ├── kotlin └── xyz │ └── deathsgun │ └── modmanager │ ├── dummy │ └── DummyModrinthProvider.kt │ ├── providers │ └── modrinth │ │ └── ModrinthTest.kt │ └── update │ └── VersionFinderTest.kt └── resources └── version ├── dynamic-fps.json ├── iris.json ├── lithium.json ├── modmanager.json ├── modmenu.json └── terra.json /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: 'DeathsGun' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Please complete the following information:** 27 | - OS: [e.g. Windows 10] 28 | - Minecraft version [e.g. 1.17.1, 21w22a] 29 | - Version [e.g. 1.0.0-alpha] 30 | 31 | **Logs** 32 | > Log files should be posted as files via drag'n'drop 33 | Latest log: 34 | 35 | Crash-report (if any): 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: 'enhancement' 6 | assignees: 'DeathsGun' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | branches: 5 | - "1.18" 6 | 7 | jobs: 8 | build: 9 | name: Build 10 | runs-on: ubuntu-latest 11 | outputs: 12 | version: ${{ steps.properties.outputs.version }} 13 | changelog: ${{ steps.properties.outputs.changelog }} 14 | steps: 15 | 16 | # Check out current repository 17 | - name: Fetch Sources 18 | uses: actions/checkout@v2.4.0 19 | 20 | # Validate wrapper 21 | - name: Gradle Wrapper Validation 22 | uses: gradle/wrapper-validation-action@v1.0.4 23 | 24 | # Setup Java 11 environment for the next steps 25 | - name: Setup Java 26 | uses: actions/setup-java@v2 27 | with: 28 | distribution: zulu 29 | java-version: 17 30 | cache: gradle 31 | 32 | # Set environment variables 33 | - name: Export Properties 34 | id: properties 35 | shell: bash 36 | run: | 37 | PROPERTIES="$(./gradlew properties --console=plain -q)" 38 | VERSION="$(echo "$PROPERTIES" | grep "^version:" | cut -f2- -d ' ')" 39 | TARGET="$(echo "$PROPERTIES" | grep "^release_target:" | cut -f2- -d ' ')" 40 | CHANGELOG="$(./gradlew getChangelog --unreleased --no-header --console=plain -q)" 41 | CHANGELOG="${CHANGELOG//'%'/'%25'}" 42 | CHANGELOG="${CHANGELOG//$'\n'/'%0A'}" 43 | CHANGELOG="${CHANGELOG//$'\r'/'%0D'}" 44 | echo "::set-output name=version::$VERSION" 45 | echo "::set-output name=changelog::$CHANGELOG" 46 | 47 | - name: Build artifact 48 | run: ./gradlew build zip 49 | 50 | - name: Publish Unit Test Results 51 | uses: EnricoMi/publish-unit-test-result-action@v1 52 | if: always() 53 | with: 54 | files: build/test-results/**/*.xml 55 | 56 | - name: Collect Artifact 57 | uses: actions/upload-artifact@v2.2.4 58 | with: 59 | path: ./build/distributions/*.zip 60 | 61 | releaseDraft: 62 | name: Release Draft 63 | if: github.event_name != 'pull_request' 64 | needs: build 65 | runs-on: ubuntu-latest 66 | steps: 67 | 68 | # Check out current repository 69 | - name: Fetch Sources 70 | uses: actions/checkout@v2.4.0 71 | 72 | # Remove old release drafts by using the curl request for the available releases with draft flag 73 | - name: Remove Old Release Drafts 74 | env: 75 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 76 | run: | 77 | gh api repos/{owner}/{repo}/releases \ 78 | --jq '.[] | select(.draft == true) | select(.target_commitish=="1.18") | .id' \ 79 | | xargs -I '{}' gh api -X DELETE repos/{owner}/{repo}/releases/{} 80 | # Create new release draft - which is not publicly visible and requires manual acceptance 81 | - name: Create Release Draft 82 | env: 83 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 84 | run: | 85 | gh release create v${{ needs.build.outputs.version }} \ 86 | --draft \ 87 | --target 1.18 \ 88 | --title "v${{ needs.build.outputs.version }}" \ 89 | --notes "$(cat << 'EOM' 90 | ${{ needs.build.outputs.changelog }} 91 | EOM 92 | )" 93 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | release: 4 | types: [ prereleased, released ] 5 | 6 | jobs: 7 | 8 | # Prepare and publish the plugin to the Marketplace repository 9 | release: 10 | name: Publish mod 11 | runs-on: ubuntu-latest 12 | steps: 13 | 14 | # Check out current repository 15 | - name: Fetch Sources 16 | uses: actions/checkout@v2.4.0 17 | with: 18 | ref: ${{ github.event.release.tag_name }} 19 | 20 | # Setup Java 17 environment for the next steps 21 | - name: Setup Java 22 | uses: actions/setup-java@v2 23 | with: 24 | distribution: zulu 25 | java-version: 17 26 | cache: gradle 27 | 28 | # Set environment variables 29 | - name: Export Properties 30 | id: properties 31 | shell: bash 32 | run: | 33 | PROPERTIES="$(./gradlew properties --console=plain -q)" 34 | TARGET="$(echo "$PROPERTIES" | grep "^release_target:" | cut -f2- -d ' ')" 35 | CHANGELOG="$(cat << 'EOM' | sed -e 's/^[[:space:]]*$//g' -e '/./,$!d' 36 | ${{ github.event.release.body }} 37 | EOM 38 | )" 39 | 40 | CHANGELOG="${CHANGELOG//'%'/'%25'}" 41 | CHANGELOG="${CHANGELOG//$'\n'/'%0A'}" 42 | CHANGELOG="${CHANGELOG//$'\r'/'%0D'}" 43 | echo "::set-output name=changelog::$CHANGELOG" 44 | # Publish the mod to Modrinth 45 | - name: Publish Plugin 46 | env: 47 | MODRINTH: ${{ secrets.MODRINTH }} 48 | run: ./gradlew publishModrinth zip 49 | 50 | # Update Unreleased section with the current release note 51 | - name: Patch Changelog 52 | if: ${{ steps.properties.outputs.changelog != '' }} 53 | env: 54 | CHANGELOG: ${{ steps.properties.outputs.changelog }} 55 | run: | 56 | ./gradlew patchChangelog --release-note="$CHANGELOG" 57 | 58 | # Upload artifact as a release asset 59 | - name: Upload Release Asset 60 | env: 61 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 62 | run: gh release upload ${{ github.event.release.tag_name }} ./build/distributions/* 63 | 64 | # Create pull request 65 | - name: Create Pull Request 66 | if: ${{ steps.properties.outputs.changelog != '' }} 67 | env: 68 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 69 | run: | 70 | VERSION="${{ github.event.release.tag_name }}" 71 | BRANCH="changelog-update-$VERSION" 72 | git config user.email "action@github.com" 73 | git config user.name "GitHub Action" 74 | git checkout -b $BRANCH 75 | git commit -am "Changelog update - $VERSION" 76 | git push --set-upstream origin $BRANCH 77 | gh pr create \ 78 | --title "Changelog update - \`$VERSION\`" \ 79 | --body "Current pull request contains patched \`CHANGELOG.md\` file for the \`$VERSION\` version." \ 80 | --base "${{ needs.build.outputs.target }}" \ 81 | --head $BRANCH -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test commit 2 | on: [ pull_request ] 3 | 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: checkout repository 9 | uses: actions/checkout@v2 10 | - name: validate gradle wrapper 11 | uses: gradle/wrapper-validation-action@v1 12 | - name: setup jdk 17 13 | uses: actions/setup-java@v1 14 | with: 15 | java-version: 17 16 | - name: make gradle wrapper executable 17 | run: chmod +x ./gradlew 18 | - name: test 19 | run: ./gradlew test 20 | - name: Publish Unit Test Results 21 | uses: EnricoMi/publish-unit-test-result-action@v1 22 | if: always() 23 | with: 24 | files: build/test-results/**/*.xml 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # User-specific stuff 2 | .idea/ 3 | 4 | *.iml 5 | *.ipr 6 | *.iws 7 | 8 | # IntelliJ 9 | out/ 10 | # mpeltonen/sbt-idea plugin 11 | .idea_modules/ 12 | 13 | # JIRA plugin 14 | atlassian-ide-plugin.xml 15 | 16 | # Compiled class file 17 | *.class 18 | 19 | # Log file 20 | *.log 21 | 22 | # BlueJ files 23 | *.ctxt 24 | 25 | # Package Files # 26 | *.jar 27 | *.war 28 | *.nar 29 | *.ear 30 | *.zip 31 | *.tar.gz 32 | *.rar 33 | 34 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 35 | hs_err_pid* 36 | 37 | *~ 38 | 39 | # temporary files which can be created if a process still has a handle open of a deleted file 40 | .fuse_hidden* 41 | 42 | # KDE directory preferences 43 | .directory 44 | 45 | # Linux trash folder which might appear on any partition or disk 46 | .Trash-* 47 | 48 | # .nfs files are created when an open file is removed but is still being accessed 49 | .nfs* 50 | 51 | # General 52 | .DS_Store 53 | .AppleDouble 54 | .LSOverride 55 | 56 | # Thumbnails 57 | ._* 58 | 59 | # Files that might appear in the root of a volume 60 | .DocumentRevisions-V100 61 | .fseventsd 62 | .Spotlight-V100 63 | .TemporaryItems 64 | .Trashes 65 | .VolumeIcon.icns 66 | .com.apple.timemachine.donotpresent 67 | 68 | # Directories potentially created on remote AFP share 69 | .AppleDB 70 | .AppleDesktop 71 | Network Trash Folder 72 | Temporary Items 73 | .apdisk 74 | 75 | # Windows thumbnail cache files 76 | Thumbs.db 77 | Thumbs.db:encryptable 78 | ehthumbs.db 79 | ehthumbs_vista.db 80 | 81 | # Dump file 82 | *.stackdump 83 | 84 | # Folder config file 85 | [Dd]esktop.ini 86 | 87 | # Recycle Bin used on file shares 88 | $RECYCLE.BIN/ 89 | 90 | # Windows Installer files 91 | *.cab 92 | *.msi 93 | *.msix 94 | *.msm 95 | *.msp 96 | 97 | # Windows shortcuts 98 | *.lnk 99 | 100 | .gradle 101 | build/ 102 | 103 | # Ignore Gradle GUI config 104 | gradle-app.setting 105 | 106 | # Cache of project 107 | .gradletasknamecache 108 | 109 | **/build/ 110 | 111 | # Common working directory 112 | run/ 113 | logs/ 114 | 115 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 116 | !gradle-wrapper.jar 117 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [Unreleased] 4 | ### Added 5 | 6 | ### Changed 7 | 8 | ### Deprecated 9 | 10 | ### Removed 11 | 12 | ### Fixed 13 | 14 | ### Security 15 | 16 | ## [1.2.3+1.18] 17 | ### Added 18 | - Support for ModMenu 3.1.0 19 | 20 | ### Changed 21 | 22 | ### Deprecated 23 | 24 | ### Removed 25 | 26 | ### Fixed 27 | 28 | ### Security 29 | 30 | ## [1.2.2+1.18] 31 | ### Fixed 32 | - Screen getting spammed by update notifications 33 | 34 | ## [1.2.1+1.18] 35 | ### Added 36 | - Update all button. Update all your mods in one go! [#100](https://github.com/DeathsGun/ModManager/issues/100) 37 | - Support for 1.16 38 | 39 | 40 | ### Fixed 41 | - Race condition when showing the update notification [#108](https://github.com/DeathsGun/ModManager/issues/108) 42 | - Mods showing up that are actually up-to-date [#101](https://github.com/DeathsGun/ModManager/issues/101) 43 | 44 | 45 | ### Changed 46 | - Updated to 1.18.1 47 | - Updated Russian translation (thanks to Felix14-v2) [#97](https://github.com/DeathsGun/ModManager/pull/97) 48 | 49 | ## [1.2.0+1.18-pre] - 22.11.2021 50 | ### Added 51 | - Compatibility with 1.18-pre releases 52 | 53 | 54 | ### Fixed 55 | - Essentials compatibility [#84](https://github.com/DeathsGun/ModManager/issues/84) 56 | - preLaunch errors [#95](https://github.com/DeathsGun/ModManager/issues/95) 57 | 58 | ## [1.2.0-alpha] - 05.11.2021 59 | ### Added 60 | - A back button [#87](https://github.com/DeathsGun/ModManager/issues/87) 61 | - New browsing experience by allowing more detailed filters [#79](https://github.com/DeathsGun/ModManager/issues/79) 62 | - Hide mods being shown in the updatable mods section [#77](https://github.com/DeathsGun/ModManager/issues/77) 63 | 64 | 65 | ### Fixed 66 | - Delete mods on preLaunch which should avoid the update 67 | problem [#91](https://github.com/DeathsGun/ModManager/issues/91) 68 | 69 | 70 | ### Changed 71 | - Minimum ```fabric-loader``` version is now 0.12 72 | 73 | ## [1.1.1-alpha] - 06.10.2021 74 | ### Fixed 75 | - Old versions not being deleted (Now really) [#51](https://github.com/DeathsGun/ModManager/issues/51) 76 | - Whitespaces producing errors [#70](https://github.com/DeathsGun/ModManager/issues/70) 77 | - Tabs producing errors [#67](https://github.com/DeathsGun/ModManager/issues/67) 78 | 79 | 80 | ### Changed 81 | - Updated turkish translation (thanks to kuzeeeyk) [#75](https://github.com/DeathsGun/ModManager/pull/75) 82 | 83 | ## [1.1.0-alpha] - 01.10.2021 84 | ### Added 85 | - Allows mods to specify their Modrinth project id [#8](https://github.com/DeathsGun/ModManager/issues/8) 86 | - Icons are now cached through restarts (Max: 10 MB) [#24](https://github.com/DeathsGun/ModManager/issues/24) 87 | - Restart notification when mods get updated, removed or 88 | installed [#30](https://github.com/DeathsGun/ModManager/issues/30) 89 | - Continue scrolling in the mod list [#38](https://github.com/DeathsGun/ModManager/issues/38) 90 | - Show changelog, current version and target version before 91 | update [#41](https://github.com/DeathsGun/ModManager/issues/41) 92 | - Sort mods by relevance, downloads, updated and newest [#45](https://github.com/DeathsGun/ModManager/issues/45) 93 | - Allows mods to disable update checking for their mod [#62](https://github.com/DeathsGun/ModManager/issues/62) 94 | 95 | 96 | ### Fixed 97 | - NullPointerException when updating mods [#42](https://github.com/DeathsGun/ModManager/issues/42) 98 | - NullPointerException when mods not follow SemVer [#61](https://github.com/DeathsGun/ModManager/issues/64) 99 | - Forge versions shown as update [#56](https://github.com/DeathsGun/ModManager/issues/56) 100 | - Old versions not being deleted [#51](https://github.com/DeathsGun/ModManager/issues/51) 101 | - Mods shown outdated but there actually up to date [#52](https://github.com/DeathsGun/ModManager/issues/52) 102 | 103 | 104 | ### Changed 105 | - Rewrite in Kotlin [#44](https://github.com/DeathsGun/ModManager/pull/44) 106 | 107 | ## [1.0.2-alpha] - 03.09.2021 108 | ### Added 109 | - New loading icon [#40](https://github.com/DeathsGun/ModManager/pull/40) 110 | - Chinese translation (Special thanks to MineCommanderCN) [#36](https://github.com/DeathsGun/ModManager/pull/36) 111 | - Korean translation (Special thanks to arlytical#1) [#32](https://github.com/DeathsGun/ModManager/pull/32) 112 | - Russian translation (Special thanks to Felix14-v2) [#31](https://github.com/DeathsGun/ModManager/pull/31) 113 | 114 | 115 | ### Fixed 116 | - CPU overload when using ModManager [#48](https://github.com/DeathsGun/ModManager/issues/48) 117 | - Forge artifacts being downloaded [#37](https://github.com/DeathsGun/ModManager/pull/37) 118 | - NullPointerException's while updating a mod [#34](https://github.com/DeathsGun/ModManager/issues/34) 119 | 120 | 121 | ### Changed 122 | - Improved Turkish translation (Special thanks to kuzeeeyk) [#39](https://github.com/DeathsGun/ModManager/pull/39) 123 | 124 | ## [1.0.1-alpha] - 24.08.2021 125 | ### Added 126 | - Turkish translation (Special thanks to kuzeeeyk) [#21](https://github.com/DeathsGun/ModManager/pull/21) 127 | - Only show "Updatable mods" category when there are updatable 128 | mods [#10](https://github.com/DeathsGun/ModManager/issues/10) 129 | 130 | 131 | ### Fixed 132 | - Crashes when opening ModMenu [#13](https://github.com/DeathsGun/ModManager/issues/13) 133 | - Update error on Windows because of file locks [#17](https://github.com/DeathsGun/ModManager/issues/13) 134 | - Search only when enter key was hit for improved performance [#7](https://github.com/DeathsGun/ModManager/issues/7) 135 | - Crashes when ModManager loses connection while opening a more detailed 136 | view [#16](https://github.com/DeathsGun/ModManager/issues/16) 137 | - Icons being mixed up [#22](https://github.com/DeathsGun/ModManager/issues/22) 138 | - Unknown mods showing up [#18](https://github.com/DeathsGun/ModManager/issues/18) 139 | 140 | ## [1.0.0-alpha] - 23.08.2021 141 | ### Added 142 | - Browsing through Modrinth 143 | - Install, remove and update mods 144 | - Notification about updates 145 | - Overview about mods that can be updated -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # ModManager 2 | 3 | Extends [Mod Menu](https://github.com/TerraformersMC/ModMenu) with a new tab for installing, 4 | removing and updating mods. 5 | 6 | Features: 7 | * Browse through Modrinth in minecraft 8 | * Install, remove and update mods in minecraft (needs restart to apply changes) 9 | * Notify about outdated mods 10 | * Show a list of all outdated mods 11 | 12 | ### Screenshots 13 | 14 | ![](screenshots/mod-overview.png) 15 | Select multiple categories: 16 | ![](screenshots/categories.png) 17 | Search view: 18 | ![](screenshots/search.png) 19 | Updatable mods list: 20 | ![](screenshots/updatable-mods.png) 21 | Mod Update: 22 | ![](screenshots/mod-update.png) 23 | 24 | ### Credits 25 | 26 | - [Prospector](https://github.com/Prospector) for creating ModMenu 27 | - [Modrinth](https://modrinth.com) for creating a public and easy to use API for searching mods 28 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 DeathsGun 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | //file:noinspection GroovyAssignabilityCheck 18 | //file:noinspection GroovyAccessibility 19 | plugins { 20 | id "fabric-loom" version "0.10-SNAPSHOT" 21 | id "com.modrinth.minotaur" version "1.2.1" 22 | id "org.jetbrains.kotlin.jvm" version "1.5.30" 23 | id "org.jetbrains.changelog" version "1.3.1" 24 | id "org.jetbrains.kotlin.plugin.serialization" version "1.5.30" 25 | } 26 | 27 | sourceCompatibility = JavaVersion.VERSION_17 28 | targetCompatibility = JavaVersion.VERSION_17 29 | 30 | archivesBaseName = project.archives_base_name 31 | version = project.mod_version 32 | group = project.maven_group 33 | 34 | repositories { 35 | maven { 36 | name = "TerraformersMC" 37 | url = "https://maven.terraformersmc.com/releases/" 38 | } 39 | mavenCentral() 40 | } 41 | 42 | dependencies { 43 | minecraft "com.mojang:minecraft:${project.minecraft_version}" 44 | mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2" 45 | modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" 46 | 47 | modImplementation "net.fabricmc:fabric-language-kotlin:${project.fabric_kotlin_version}" 48 | modImplementation "com.terraformersmc:modmenu:${project.modmenu_version}" 49 | 50 | testImplementation 'org.jetbrains.kotlin:kotlin-test:1.5.31' 51 | } 52 | 53 | processResources { 54 | inputs.property "version", project.version 55 | inputs.property "releaseTarget", project.release_target 56 | inputs.property "modmenu_version", project.modmenu_version 57 | 58 | filesMatching("fabric.mod.json") { 59 | expand project.properties 60 | } 61 | filesMatching("build.info") { 62 | expand project.properties 63 | } 64 | } 65 | 66 | test { 67 | useJUnitPlatform() 68 | } 69 | 70 | tasks.withType(JavaCompile).configureEach { 71 | it.options.encoding = "UTF-8" 72 | // Minecraft 1.17 (21w19a) upwards uses Java 16. 73 | it.options.release = 16 74 | } 75 | 76 | compileKotlin { 77 | kotlinOptions.jvmTarget = "16" 78 | kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" 79 | } 80 | 81 | compileTestKotlin { 82 | kotlinOptions.jvmTarget = "16" 83 | kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" 84 | } 85 | 86 | java { 87 | withSourcesJar() 88 | } 89 | 90 | jar { 91 | from("LICENSE") { 92 | rename { "${it}_${project.archivesBaseName}" } 93 | } 94 | } 95 | 96 | task zip(type: Zip, dependsOn: remapJar) { 97 | from remapJar 98 | } 99 | build.finalizedBy(zip) 100 | 101 | 102 | import com.modrinth.minotaur.TaskModrinthUpload 103 | import com.modrinth.minotaur.request.Dependency 104 | import com.modrinth.minotaur.request.VersionType 105 | 106 | task publishModrinth(type: TaskModrinthUpload, dependsOn: remapJar) { 107 | onlyIf { 108 | System.getenv("MODRINTH") 109 | } 110 | 111 | token = System.getenv("MODRINTH") 112 | projectId = "6kq7BzRK" 113 | versionNumber = version 114 | uploadFile = remapJar 115 | versionType = VersionType.RELEASE 116 | addGameVersion(project.release_target) 117 | addLoader("fabric") 118 | addDependency("JPP6w2U1", Dependency.DependencyType.REQUIRED) // Mod Menu 119 | addDependency("1qsZV7U7", Dependency.DependencyType.REQUIRED) // fabric-language-kotlin 120 | changelog = this.changelog.getUnreleased().withHeader(false).toText() 121 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2021 DeathsGun 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | org.gradle.jvmargs=-Xms2G -Xmx4G 17 | # Fabric Properties 18 | # check this on https://modmuss50.me/fabric.html 19 | release_target=1.18 20 | minecraft_version=1.18.1 21 | yarn_mappings=1.18.1+build.17 22 | loader_version=0.12.12 23 | # Mod Properties 24 | mod_version=1.2.3+1.18 25 | maven_group=xyz.deathsgun 26 | archives_base_name=modmanager 27 | # Dependencies 28 | modmenu_version=3.0.0 29 | # Kotlin 30 | fabric_kotlin_version=1.6.4+kotlin.1.5.30 -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModManagerMC/ModManager/c6cd9535504837a97fef3b73d43df3a6c95a2ca3/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /screenshots/categories.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModManagerMC/ModManager/c6cd9535504837a97fef3b73d43df3a6c95a2ca3/screenshots/categories.png -------------------------------------------------------------------------------- /screenshots/mod-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModManagerMC/ModManager/c6cd9535504837a97fef3b73d43df3a6c95a2ca3/screenshots/mod-overview.png -------------------------------------------------------------------------------- /screenshots/mod-update.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModManagerMC/ModManager/c6cd9535504837a97fef3b73d43df3a6c95a2ca3/screenshots/mod-update.png -------------------------------------------------------------------------------- /screenshots/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModManagerMC/ModManager/c6cd9535504837a97fef3b73d43df3a6c95a2ca3/screenshots/search.png -------------------------------------------------------------------------------- /screenshots/updatable-mods.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModManagerMC/ModManager/c6cd9535504837a97fef3b73d43df3a6c95a2ca3/screenshots/updatable-mods.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 DeathsGun 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | pluginManagement { 18 | repositories { 19 | maven { 20 | name = 'Fabric' 21 | url = 'https://maven.fabricmc.net/' 22 | } 23 | gradlePluginPortal() 24 | } 25 | } 26 | rootProject.name = "modmanager" 27 | -------------------------------------------------------------------------------- /src/main/java/xyz/deathsgun/modmanager/ModMenuEntrypoint.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 DeathsGun 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package xyz.deathsgun.modmanager; 18 | 19 | import com.terraformersmc.modmenu.api.ConfigScreenFactory; 20 | import com.terraformersmc.modmenu.api.ModMenuApi; 21 | import xyz.deathsgun.modmanager.gui.ConfigScreen; 22 | 23 | public class ModMenuEntrypoint implements ModMenuApi { 24 | @Override 25 | public ConfigScreenFactory getModConfigScreenFactory() { 26 | return ConfigScreen::new; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/xyz/deathsgun/modmanager/mixin/ModsScreenMixin.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 DeathsGun 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package xyz.deathsgun.modmanager.mixin; 18 | 19 | import com.terraformersmc.modmenu.gui.ModsScreen; 20 | import com.terraformersmc.modmenu.gui.widget.ModMenuTexturedButtonWidget; 21 | import com.terraformersmc.modmenu.gui.widget.entries.ModListEntry; 22 | import net.minecraft.client.MinecraftClient; 23 | import net.minecraft.client.gui.screen.Screen; 24 | import net.minecraft.text.LiteralText; 25 | import net.minecraft.text.Text; 26 | import net.minecraft.text.TranslatableText; 27 | import net.minecraft.util.Identifier; 28 | import org.spongepowered.asm.mixin.Mixin; 29 | import org.spongepowered.asm.mixin.Shadow; 30 | import org.spongepowered.asm.mixin.injection.At; 31 | import org.spongepowered.asm.mixin.injection.Inject; 32 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 33 | import xyz.deathsgun.modmanager.ModManager; 34 | import xyz.deathsgun.modmanager.config.Config; 35 | import xyz.deathsgun.modmanager.gui.ModsOverviewScreen; 36 | import xyz.deathsgun.modmanager.gui.widget.TexturedButton; 37 | 38 | import java.util.Map; 39 | 40 | @Mixin(ModsScreen.class) 41 | public abstract class ModsScreenMixin extends Screen { 42 | 43 | private static final Identifier MODMANAGER_BUTTON_LOCATION = new Identifier("modmanager", "textures/gui/install_button.png"); 44 | private static final Identifier MODMANAGER_HIDE_BUTTON = new Identifier("modmanager", "textures/gui/hide_button.png"); 45 | private static final Identifier MODMANAGER_SHOW_BUTTON = new Identifier("modmanager", "textures/gui/show_button.png"); 46 | @Shadow 47 | private int paneWidth; 48 | @Shadow 49 | private int paneY; 50 | 51 | @Shadow 52 | private ModListEntry selected; 53 | 54 | @Shadow 55 | public abstract Map getModHasConfigScreen(); 56 | 57 | private TexturedButton hideButton; 58 | 59 | protected ModsScreenMixin(Text title) { 60 | super(title); 61 | } 62 | 63 | @Inject(method = "init", at = @At("TAIL")) 64 | public void onInit(CallbackInfo ci) { 65 | int searchBoxWidth = this.paneWidth - 32 - 22; 66 | this.addDrawableChild(new ModMenuTexturedButtonWidget(this.paneWidth / 2 + searchBoxWidth / 2 + 14, 67 | 22, 20, 20, 0, 0, MODMANAGER_BUTTON_LOCATION, 32, 64, 68 | button -> MinecraftClient.getInstance().setScreen(new ModsOverviewScreen(this)), LiteralText.EMPTY, 69 | (button, matrices, mouseX, mouseY) -> { 70 | if (!button.isHovered()) { 71 | return; 72 | } 73 | this.renderTooltip(matrices, new TranslatableText("modmanager.button.open"), mouseX, mouseY); 74 | })); 75 | this.hideButton = this.addDrawableChild(new TexturedButton(width - 24 - 22, paneY, 20, 20, 0, 76 | 0, MODMANAGER_HIDE_BUTTON, 32, 64, button -> { 77 | if (ModManager.modManager.getConfig().getHidden().contains(selected.getMod().getId())) { 78 | ModManager.modManager.getConfig().getHidden().remove(selected.getMod().getId()); 79 | } else { 80 | ModManager.modManager.getConfig().getHidden().add(selected.getMod().getId()); 81 | } 82 | Config.Companion.saveConfig(ModManager.modManager.getConfig()); 83 | }, ((button, matrices, mouseX, mouseY) -> { 84 | if (!hideButton.isJustHovered() || !button.isHovered()) { 85 | return; 86 | } 87 | TranslatableText text = new TranslatableText("modmanager.button.hide"); 88 | if (ModManager.modManager.getConfig().getHidden().contains(selected.getMod().getId())) { 89 | text = new TranslatableText("modmanager.button.show"); 90 | } 91 | this.renderTooltip(matrices, text, mouseX, mouseY); 92 | }))); 93 | } 94 | 95 | @Inject(method = "tick", at = @At("HEAD")) 96 | public void onTick(CallbackInfo ci) { 97 | this.hideButton.visible = ModManager.modManager.getUpdate().getUpdates() 98 | .stream().anyMatch(it -> it.getFabricId().equalsIgnoreCase(selected.mod.getId())); 99 | if (ModManager.modManager.getConfig().getHidden().contains(selected.getMod().getId())) { 100 | this.hideButton.setImage(MODMANAGER_SHOW_BUTTON); 101 | } else { 102 | this.hideButton.setImage(MODMANAGER_HIDE_BUTTON); 103 | } 104 | this.hideButton.x = getModHasConfigScreen().getOrDefault(selected.getMod().getId(), false) ? width - 24 - 22 : width - 24; 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /src/main/kotlin/xyz/deathsgun/modmanager/ModManager.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 DeathsGun 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package xyz.deathsgun.modmanager 18 | 19 | import kotlinx.coroutines.DelicateCoroutinesApi 20 | import kotlinx.coroutines.GlobalScope 21 | import kotlinx.coroutines.launch 22 | import net.fabricmc.api.ClientModInitializer 23 | import net.minecraft.SharedConstants 24 | import xyz.deathsgun.modmanager.api.mod.State 25 | import xyz.deathsgun.modmanager.api.provider.IModProvider 26 | import xyz.deathsgun.modmanager.api.provider.IModUpdateProvider 27 | import xyz.deathsgun.modmanager.config.Config 28 | import xyz.deathsgun.modmanager.icon.IconCache 29 | import xyz.deathsgun.modmanager.providers.modrinth.Modrinth 30 | import xyz.deathsgun.modmanager.state.SavedState 31 | import xyz.deathsgun.modmanager.update.UpdateManager 32 | import java.util.* 33 | 34 | class ModManager : ClientModInitializer { 35 | 36 | private val states = ArrayList() 37 | var config: Config = Config.loadConfig() 38 | var changed: Boolean = false 39 | val update: UpdateManager = UpdateManager() 40 | val icons: IconCache = IconCache() 41 | val provider: HashMap = HashMap() 42 | val updateProvider: HashMap = HashMap() 43 | 44 | init { 45 | val modrinth = Modrinth() 46 | provider[modrinth.getName().lowercase()] = modrinth 47 | updateProvider[modrinth.getName().lowercase()] = modrinth 48 | } 49 | 50 | companion object { 51 | private val properties = Properties().apply { 52 | load(Companion::class.java.getResourceAsStream("/build.info")) 53 | } 54 | @JvmField 55 | var shownUpdateNotification: Boolean = false 56 | 57 | @JvmStatic 58 | lateinit var modManager: ModManager 59 | 60 | @JvmStatic 61 | fun getVersion(): String { 62 | return properties.getProperty("version") 63 | } 64 | 65 | @JvmStatic 66 | fun getMinecraftReleaseTarget(): String { 67 | return properties.getProperty("releaseTarget") 68 | } 69 | 70 | @JvmStatic 71 | fun getMinecraftVersionId(): String { 72 | return SharedConstants.RELEASE_TARGET 73 | } 74 | } 75 | 76 | @OptIn(DelicateCoroutinesApi::class) 77 | override fun onInitializeClient() { 78 | GlobalScope.launch { 79 | icons.cleanupCache() 80 | } 81 | } 82 | 83 | fun setModState(fabricId: String, modId: String, state: State) { 84 | this.states.removeAll { it.modId == modId || it.fabricId == fabricId } 85 | this.states.add(SavedState(fabricId, modId, state)) 86 | } 87 | 88 | fun getModState(id: String): State { 89 | return this.states.find { it.modId == id || it.fabricId == id }?.state ?: State.DOWNLOADABLE 90 | } 91 | 92 | fun getSelectedProvider(): IModProvider? { 93 | return this.provider[config.defaultProvider] 94 | } 95 | 96 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/deathsgun/modmanager/PreLaunchHook.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 DeathsGun 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package xyz.deathsgun.modmanager 18 | 19 | import kotlinx.coroutines.DelicateCoroutinesApi 20 | import kotlinx.coroutines.GlobalScope 21 | import kotlinx.coroutines.launch 22 | import kotlinx.serialization.ExperimentalSerializationApi 23 | import kotlinx.serialization.decodeFromString 24 | import kotlinx.serialization.json.Json 25 | import net.fabricmc.loader.api.FabricLoader 26 | import net.fabricmc.loader.api.entrypoint.PreLaunchEntrypoint 27 | import org.apache.logging.log4j.LogManager 28 | import java.nio.file.Files 29 | import kotlin.io.path.Path 30 | import kotlin.io.path.deleteIfExists 31 | 32 | class PreLaunchHook : PreLaunchEntrypoint { 33 | 34 | private val logger = LogManager.getLogger("ModManager") 35 | 36 | @OptIn(DelicateCoroutinesApi::class) 37 | override fun onPreLaunch() { 38 | ModManager.modManager = ModManager() 39 | GlobalScope.launch { 40 | ModManager.modManager.update.checkUpdates() 41 | } 42 | val filesToDelete = try { 43 | loadFiles() 44 | } catch (e: Exception) { 45 | ArrayList() 46 | } 47 | for (file in filesToDelete) { 48 | logger.info("Deleting {}", file) 49 | val path = Path(file) 50 | try { 51 | Files.delete(path) 52 | } catch (e: Exception) { // Ignore it 53 | } 54 | } 55 | } 56 | 57 | @OptIn(ExperimentalSerializationApi::class) 58 | private fun loadFiles(): ArrayList { 59 | val configFile = FabricLoader.getInstance().configDir.resolve(".modmanager.delete.json") 60 | if (Files.notExists(configFile)) { 61 | return ArrayList() 62 | } 63 | val data = Files.readString(configFile, Charsets.UTF_8) 64 | configFile.deleteIfExists() 65 | return Json.decodeFromString(data) 66 | } 67 | 68 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/deathsgun/modmanager/api/ModInstallResult.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 DeathsGun 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package xyz.deathsgun.modmanager.api 18 | 19 | import net.minecraft.text.TranslatableText 20 | 21 | sealed class ModInstallResult { 22 | 23 | object Success : ModInstallResult() 24 | data class Error(val text: TranslatableText, val cause: Exception? = null) : ModInstallResult() 25 | } 26 | -------------------------------------------------------------------------------- /src/main/kotlin/xyz/deathsgun/modmanager/api/ModRemoveResult.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 DeathsGun 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package xyz.deathsgun.modmanager.api 18 | 19 | import net.minecraft.text.TranslatableText 20 | 21 | sealed class ModRemoveResult { 22 | object Success : ModRemoveResult() 23 | data class Error(val text: TranslatableText, val cause: Exception? = null) : ModRemoveResult() 24 | } 25 | -------------------------------------------------------------------------------- /src/main/kotlin/xyz/deathsgun/modmanager/api/ModUpdateResult.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 DeathsGun 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package xyz.deathsgun.modmanager.api 18 | 19 | import net.minecraft.text.TranslatableText 20 | 21 | sealed class ModUpdateResult { 22 | object Success : ModUpdateResult() 23 | data class Error(val text: TranslatableText, val cause: Exception? = null) : ModUpdateResult() 24 | } 25 | -------------------------------------------------------------------------------- /src/main/kotlin/xyz/deathsgun/modmanager/api/gui/list/IListScreen.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 DeathsGun 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package xyz.deathsgun.modmanager.api.gui.list 18 | 19 | import net.minecraft.client.gui.Element 20 | 21 | interface IListScreen { 22 | 23 | fun getFocused(): Element? 24 | 25 | fun updateSelectedEntry(widget: Any, entry: E?) 26 | 27 | fun updateMultipleEntries(widget: Any, entries: ArrayList) 28 | 29 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/deathsgun/modmanager/api/gui/list/MultiSelectListWidget.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 DeathsGun 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package xyz.deathsgun.modmanager.api.gui.list 18 | 19 | import kotlinx.serialization.ExperimentalSerializationApi 20 | import net.minecraft.client.MinecraftClient 21 | 22 | /** 23 | * Using ModMenu's implementation because the implementation from 24 | * Mojang is broken. [com.terraformersmc.modmenu.gui.ModsScreen]. 25 | * All credits for this code go to the Terraformer's 26 | */ 27 | abstract class MultiSelectListWidget>( 28 | client: MinecraftClient, 29 | width: Int, 30 | height: Int, 31 | top: Int, 32 | bottom: Int, 33 | itemHeight: Int, 34 | parent: IListScreen 35 | ) : ListWidget(client, width, height, top, bottom, itemHeight, parent) { 36 | 37 | private var selectedIds = ArrayList() 38 | 39 | @OptIn(ExperimentalSerializationApi::class) 40 | override fun setSelected(entry: E?) { 41 | super.setSelected(entry) 42 | if (entry == null) { 43 | return 44 | } 45 | if (selectedIds.contains(entry.id)) { 46 | selectedIds.removeIf { it == entry.id } 47 | } else { 48 | selectedIds.add(entry.id) 49 | } 50 | parent.updateMultipleEntries( 51 | this, 52 | ArrayList(children().filter { selectedIds.contains(it.id) }.sortedBy { selectedIds.indexOf(it.id) }) 53 | ) 54 | } 55 | 56 | fun setSelected(entries: List) { 57 | selectedIds.clear() 58 | for (entry in entries) { 59 | setSelected(entry) 60 | } 61 | } 62 | 63 | override fun isSelectedEntry(index: Int): Boolean { 64 | return selectedIds.contains(getEntry(index).id) 65 | } 66 | 67 | override fun addEntry(entry: E): Int { 68 | val i = super.addEntry(entry) 69 | if (selectedIds.contains(entry.id)) { 70 | setSelected(entry) 71 | } 72 | return i 73 | } 74 | 75 | override fun isSelectedEntry(entry: ListWidget.Entry): Boolean { 76 | return selectedIds.contains(entry.id) 77 | } 78 | 79 | abstract class Entry>(list: MultiSelectListWidget, id: String) : 80 | ListWidget.Entry(list, id) { 81 | @Suppress("UNCHECKED_CAST") 82 | override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean { 83 | list.setSelected(this as E) 84 | return true 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/deathsgun/modmanager/api/http/CategoriesResult.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 DeathsGun 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package xyz.deathsgun.modmanager.api.http 18 | 19 | import net.minecraft.text.TranslatableText 20 | import xyz.deathsgun.modmanager.api.mod.Category 21 | 22 | sealed class CategoriesResult { 23 | 24 | data class Success(val categories: List) : CategoriesResult() 25 | 26 | data class Error(val text: TranslatableText, val cause: Exception? = null) : CategoriesResult() 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/kotlin/xyz/deathsgun/modmanager/api/http/HttpClient.kt: -------------------------------------------------------------------------------- 1 | package xyz.deathsgun.modmanager.api.http 2 | 3 | import java.io.ByteArrayInputStream 4 | import java.io.InputStream 5 | import java.net.HttpURLConnection 6 | import java.net.URI 7 | import java.net.URL 8 | import java.nio.file.Files 9 | import java.nio.file.Path 10 | import kotlin.math.min 11 | 12 | object HttpClient { 13 | 14 | fun get(url: String): ByteArray { 15 | return get(URI.create(url)) 16 | } 17 | 18 | fun get(uri: URI): ByteArray { 19 | val connection = uri.toURL().openConnection() as HttpURLConnection 20 | connection.readTimeout = 10000 21 | connection.requestMethod = "GET" 22 | connection.connect() 23 | if (connection.responseCode != 200) { 24 | connection.disconnect() 25 | throw InvalidStatusCodeException(connection.responseCode) 26 | } 27 | val content = connection.inputStream.readBytes() 28 | connection.disconnect() 29 | return content 30 | } 31 | 32 | fun getInputStream(url: String): InputStream { 33 | return getInputStream(URI.create(url)) 34 | } 35 | 36 | private fun getInputStream(uri: URI): InputStream { 37 | return ByteArrayInputStream(get(uri)) 38 | } 39 | 40 | fun download(url: String, path: Path, listener: ((Double) -> Unit)? = null) { 41 | val output = Files.newOutputStream(path) 42 | val connection = URL(url).openConnection() as HttpURLConnection 43 | connection.readTimeout = 10000 44 | connection.requestMethod = "GET" 45 | connection.connect() 46 | if (connection.responseCode != 200) { 47 | connection.disconnect() 48 | throw InvalidStatusCodeException(connection.responseCode) 49 | } 50 | val size = connection.contentLength 51 | var downloaded = 0 52 | while (true) { 53 | val buffer = ByteArray(min(1024, size - downloaded)) 54 | val read = connection.inputStream.read(buffer) 55 | if (read == -1) { 56 | break 57 | } 58 | output.write(buffer, 0, read) 59 | downloaded += read 60 | listener?.invoke((downloaded / size).toDouble()) 61 | } 62 | connection.disconnect() 63 | output.flush() 64 | output.close() 65 | } 66 | 67 | class InvalidStatusCodeException(val statusCode: Int) : Exception("Received invalid status code: $statusCode") 68 | } 69 | -------------------------------------------------------------------------------- /src/main/kotlin/xyz/deathsgun/modmanager/api/http/ModResult.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 DeathsGun 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package xyz.deathsgun.modmanager.api.http 18 | 19 | import net.minecraft.text.TranslatableText 20 | import xyz.deathsgun.modmanager.api.mod.Mod 21 | 22 | sealed class ModResult { 23 | data class Success(val mod: Mod) : ModResult() 24 | data class Error(val text: TranslatableText, val cause: Exception? = null) : ModResult() 25 | } 26 | -------------------------------------------------------------------------------- /src/main/kotlin/xyz/deathsgun/modmanager/api/http/ModsResult.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 DeathsGun 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package xyz.deathsgun.modmanager.api.http 18 | 19 | import net.minecraft.text.TranslatableText 20 | import xyz.deathsgun.modmanager.api.mod.Mod 21 | 22 | sealed class ModsResult { 23 | data class Success(val mods: List) : ModsResult() 24 | data class Error(val text: TranslatableText, val cause: Exception? = null) : ModsResult() 25 | } 26 | -------------------------------------------------------------------------------- /src/main/kotlin/xyz/deathsgun/modmanager/api/http/VersionResult.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 DeathsGun 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package xyz.deathsgun.modmanager.api.http 18 | 19 | import net.minecraft.text.TranslatableText 20 | import xyz.deathsgun.modmanager.api.mod.Version 21 | 22 | sealed class VersionResult { 23 | 24 | data class Success(val versions: List) : VersionResult() 25 | data class Error(val text: TranslatableText, val cause: Exception? = null) : VersionResult() 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/kotlin/xyz/deathsgun/modmanager/api/mod/Asset.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 DeathsGun 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package xyz.deathsgun.modmanager.api.mod 18 | 19 | data class Asset( 20 | val url: String, 21 | val filename: String, 22 | val hashes: Map, 23 | val primary: Boolean 24 | ) 25 | -------------------------------------------------------------------------------- /src/main/kotlin/xyz/deathsgun/modmanager/api/mod/Category.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 DeathsGun 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package xyz.deathsgun.modmanager.api.mod 18 | 19 | import net.minecraft.text.TranslatableText 20 | 21 | data class Category( 22 | val id: String, 23 | val text: TranslatableText 24 | ) 25 | 26 | -------------------------------------------------------------------------------- /src/main/kotlin/xyz/deathsgun/modmanager/api/mod/Mod.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 DeathsGun 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package xyz.deathsgun.modmanager.api.mod 18 | 19 | data class Mod( 20 | val id: String, 21 | val slug: String, 22 | var author: String?, 23 | val name: String, 24 | var shortDescription: String, 25 | val iconUrl: String?, 26 | var description: String?, 27 | val license: String?, 28 | val categories: List, 29 | ) 30 | -------------------------------------------------------------------------------- /src/main/kotlin/xyz/deathsgun/modmanager/api/mod/State.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 DeathsGun 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package xyz.deathsgun.modmanager.api.mod 18 | 19 | enum class State { 20 | 21 | /** 22 | * Returns this if the mod has been found in the current 23 | * mod list 24 | */ 25 | INSTALLED, 26 | 27 | /** 28 | * Returns this if the mod has been found and also has been 29 | * checked for updates 30 | */ 31 | OUTDATED, 32 | 33 | /** 34 | * Returns this if the mod was not found 35 | */ 36 | DOWNLOADABLE 37 | 38 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/deathsgun/modmanager/api/mod/Version.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 DeathsGun 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package xyz.deathsgun.modmanager.api.mod 18 | 19 | import java.time.LocalDate 20 | 21 | data class Version( 22 | val version: String, 23 | val changelog: String, 24 | val releaseDate: LocalDate, 25 | val type: VersionType, 26 | val gameVersions: List, 27 | val assets: List 28 | ) 29 | -------------------------------------------------------------------------------- /src/main/kotlin/xyz/deathsgun/modmanager/api/mod/VersionType.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 DeathsGun 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package xyz.deathsgun.modmanager.api.mod 18 | 19 | enum class VersionType { 20 | ALPHA, BETA, RELEASE, UNKNOWN 21 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/deathsgun/modmanager/api/provider/IModProvider.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 DeathsGun 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package xyz.deathsgun.modmanager.api.provider 18 | 19 | import xyz.deathsgun.modmanager.api.http.CategoriesResult 20 | import xyz.deathsgun.modmanager.api.http.ModResult 21 | import xyz.deathsgun.modmanager.api.http.ModsResult 22 | import xyz.deathsgun.modmanager.api.mod.Category 23 | import xyz.deathsgun.modmanager.api.mod.Mod 24 | 25 | interface IModProvider : IModUpdateProvider { 26 | 27 | /** 28 | * Returns a list of all possible mod categories also with an translatable text 29 | * 30 | * @return a list of all mod categories 31 | */ 32 | fun getCategories(): CategoriesResult 33 | 34 | /** 35 | * Returns a limited number of [Mod]'s sorted 36 | * in a given way. 37 | * 38 | * @param sorting the requested sorting of the mods 39 | * @param page the requested from the UI starting at 1 40 | * @param limit to not overfill the ui and for shorter loading times the amount of returned mods needs to limited 41 | * @return a list of sorted mods 42 | */ 43 | fun getMods(sorting: Sorting, page: Int, limit: Int): ModsResult 44 | 45 | /** 46 | * Returns a limited number of [Mod]'s from the specified category 47 | * 48 | * @param categories the categories of the mods 49 | * @param sorting the sorting order 50 | * @param page the requested from the UI starting at 1 51 | * @param limit to not overfill the ui and for shorter loading times the amount of returned mods needs to limited 52 | * @return a list of sorted mods 53 | */ 54 | fun getMods(categories: List, sorting: Sorting, page: Int, limit: Int): ModsResult 55 | 56 | /** 57 | * Returns a limited number of [Mod]'s from a given search. 58 | * 59 | * @param query the search string 60 | * @param categories the categories in which should be searched 61 | * @param page the current requested page starts at 0 62 | * @param limit the amount of mods to return 63 | * @return a list of mods matching the search term 64 | */ 65 | fun search(query: String, categories: List, sorting: Sorting, page: Int, limit: Int): ModsResult 66 | 67 | /** 68 | * Returns a more detailed representation of the mod 69 | * 70 | * @param id the [Mod] id which is used to receive 71 | * @return a more detailed representation of [Mod] 72 | */ 73 | fun getMod(id: String): ModResult 74 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/deathsgun/modmanager/api/provider/IModUpdateProvider.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 DeathsGun 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package xyz.deathsgun.modmanager.api.provider 18 | 19 | import xyz.deathsgun.modmanager.api.http.VersionResult 20 | 21 | interface IModUpdateProvider { 22 | 23 | /** 24 | * Name of the provider. This will be shown 25 | * in the GUI 26 | * 27 | * @return returns a user-friendly name of the mod provider implementation 28 | */ 29 | fun getName(): String 30 | 31 | /** 32 | * Gets a list all versions that can be downloaded for the specified [id] 33 | * @return a returns a [VersionResult] which can be an [VersionResult.Error] or [VersionResult.Success] 34 | * which contains a list of [xyz.deathsgun.modmanager.api.mod.Version] 35 | */ 36 | fun getVersionsForMod(id: String): VersionResult 37 | 38 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/deathsgun/modmanager/api/provider/Sorting.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 DeathsGun 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package xyz.deathsgun.modmanager.api.provider 17 | 18 | import net.minecraft.text.Text 19 | import net.minecraft.text.TranslatableText 20 | 21 | enum class Sorting { 22 | RELEVANCE, DOWNLOADS, UPDATED, NEWEST; 23 | 24 | fun translations(): Text { 25 | return TranslatableText(String.format("modmanager.sorting.%s", name.lowercase())) 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/deathsgun/modmanager/config/Config.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 DeathsGun 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package xyz.deathsgun.modmanager.config 18 | 19 | import kotlinx.serialization.ExperimentalSerializationApi 20 | import kotlinx.serialization.Serializable 21 | import kotlinx.serialization.decodeFromString 22 | import kotlinx.serialization.encodeToString 23 | import kotlinx.serialization.json.Json 24 | import net.fabricmc.loader.api.FabricLoader 25 | import net.minecraft.text.Text 26 | import net.minecraft.text.TranslatableText 27 | import xyz.deathsgun.modmanager.api.mod.VersionType 28 | import java.nio.charset.Charset 29 | import java.nio.file.Files 30 | 31 | @Serializable 32 | data class Config( 33 | var defaultProvider: String = "modrinth", 34 | var updateChannel: UpdateChannel = UpdateChannel.ALL, 35 | var hidden: ArrayList = ArrayList() 36 | ) { 37 | 38 | companion object { 39 | 40 | private val json = Json { 41 | prettyPrint = true 42 | encodeDefaults = true 43 | } 44 | 45 | @OptIn(ExperimentalSerializationApi::class) 46 | fun loadConfig(): Config { 47 | return try { 48 | val file = FabricLoader.getInstance().configDir.resolve("modmanager.json") 49 | Files.createDirectories(file.parent) 50 | val data = Files.readString(file, Charset.forName("UTF-8")) 51 | json.decodeFromString(data) 52 | } catch (e: Exception) { 53 | if (e !is NoSuchFileException) { 54 | e.printStackTrace() 55 | } 56 | saveConfig(Config()) 57 | } 58 | } 59 | 60 | @OptIn(ExperimentalSerializationApi::class) 61 | fun saveConfig(config: Config): Config { 62 | try { 63 | val file = FabricLoader.getInstance().configDir.resolve("modmanager.json") 64 | val data = json.encodeToString(config) 65 | Files.writeString(file, data, Charset.forName("UTF-8")) 66 | } catch (ignored: Exception) { 67 | } 68 | return config 69 | } 70 | } 71 | 72 | enum class UpdateChannel { 73 | ALL, STABLE, UNSTABLE; 74 | 75 | fun text(): Text { 76 | return TranslatableText(String.format("modmanager.channel.%s", name.lowercase())) 77 | } 78 | 79 | fun isReleaseAllowed(type: VersionType): Boolean { 80 | if (this == ALL) { 81 | return true 82 | } 83 | if (this == STABLE && type == VersionType.RELEASE) { 84 | return true 85 | } 86 | return this == UNSTABLE 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/kotlin/xyz/deathsgun/modmanager/gui/ConfigScreen.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 DeathsGun 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package xyz.deathsgun.modmanager.gui 18 | 19 | import net.minecraft.client.font.MultilineText 20 | import net.minecraft.client.gui.screen.Screen 21 | import net.minecraft.client.gui.screen.ScreenTexts 22 | import net.minecraft.client.gui.widget.ButtonWidget 23 | import net.minecraft.client.gui.widget.CyclingButtonWidget 24 | import net.minecraft.client.util.math.MatrixStack 25 | import net.minecraft.text.LiteralText 26 | import net.minecraft.text.TranslatableText 27 | import xyz.deathsgun.modmanager.ModManager 28 | import xyz.deathsgun.modmanager.config.Config 29 | 30 | class ConfigScreen(private val previousScreen: Screen) : Screen(LiteralText("Config")) { 31 | 32 | private lateinit var defaultProvider: CyclingButtonWidget 33 | private lateinit var updateChannel: CyclingButtonWidget 34 | private var config: Config = ModManager.modManager.config.copy() 35 | 36 | override fun init() { 37 | defaultProvider = addDrawableChild(CyclingButtonWidget.builder { LiteralText(it) } 38 | .values(ModManager.modManager.provider.keys.toList()) 39 | .initially(config.defaultProvider) 40 | .build(width - 220, 30, 200, 20, TranslatableText("modmanager.button.defaultProvider")) 41 | { _: CyclingButtonWidget, s: String -> config.defaultProvider = s }) 42 | defaultProvider.active = ModManager.modManager.provider.size > 1 43 | 44 | updateChannel = addDrawableChild(CyclingButtonWidget.builder { it.text() } 45 | .values(listOf(Config.UpdateChannel.ALL, Config.UpdateChannel.STABLE)) 46 | .initially(config.updateChannel) 47 | .build(width - 220, 60, 200, 20, TranslatableText("modmanager.button.updateChannel")) 48 | { _: CyclingButtonWidget, channel: Config.UpdateChannel -> config.updateChannel = channel }) 49 | 50 | addDrawableChild(ButtonWidget( 51 | width / 2 - 154, height - 28, 150, 20, ScreenTexts.CANCEL, 52 | ) { 53 | client!!.setScreen(previousScreen) 54 | }) 55 | addDrawableChild(ButtonWidget( 56 | width / 2 + 4, height - 28, 150, 20, TranslatableText("modmanager.button.save") 57 | ) { 58 | ModManager.modManager.config = Config.saveConfig(this.config) 59 | client!!.setScreen(previousScreen) 60 | }) 61 | 62 | } 63 | 64 | override fun render(matrices: MatrixStack?, mouseX: Int, mouseY: Int, delta: Float) { 65 | super.renderBackground(matrices) 66 | 67 | MultilineText.create(textRenderer, TranslatableText("modmanager.provider.info"), width - 230) 68 | .draw(matrices, 10, 35, textRenderer.fontHeight, 0xFFFFFF) 69 | MultilineText.create(textRenderer, TranslatableText("modmanager.channel.info"), width - 230) 70 | .draw(matrices, 10, 65, textRenderer.fontHeight, 0xFFFFFF) 71 | super.render(matrices, mouseX, mouseY, delta) 72 | } 73 | 74 | override fun onClose() { 75 | client!!.setScreen(previousScreen) 76 | } 77 | 78 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/deathsgun/modmanager/gui/ErrorScreen.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 DeathsGun 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package xyz.deathsgun.modmanager.gui 18 | 19 | import net.minecraft.client.font.MultilineText 20 | import net.minecraft.client.gui.DrawableHelper 21 | import net.minecraft.client.gui.screen.Screen 22 | import net.minecraft.client.gui.screen.ScreenTexts 23 | import net.minecraft.client.gui.widget.ButtonWidget 24 | import net.minecraft.client.util.math.MatrixStack 25 | import net.minecraft.text.TranslatableText 26 | import net.minecraft.util.math.MathHelper 27 | 28 | class ErrorScreen( 29 | private val previousScreen: Screen, 30 | private val actionScreen: Screen, 31 | private val error: TranslatableText 32 | ) : 33 | Screen(TranslatableText("modmanager.error.title")) { 34 | 35 | private lateinit var text: MultilineText 36 | 37 | override fun init() { 38 | this.text = MultilineText.create(this.textRenderer, this.error, this.width - 50) 39 | val linesHeight = this.text.count() * 9 40 | val bottom = MathHelper.clamp(90 + linesHeight + 12, this.height / 6 + 69, this.height - 24) 41 | addDrawableChild( 42 | ButtonWidget( 43 | this.width / 2 - 155, 44 | bottom, 45 | 150, 46 | 20, 47 | ScreenTexts.BACK 48 | ) { 49 | client!!.setScreen(previousScreen) 50 | } 51 | ) 52 | addDrawableChild( 53 | ButtonWidget( 54 | this.width / 2 + 5, 55 | bottom, 56 | 150, 57 | 20, 58 | TranslatableText("modmanager.button.tryAgain") 59 | ) { 60 | client!!.setScreen(actionScreen) 61 | } 62 | ) 63 | } 64 | 65 | override fun render(matrices: MatrixStack?, mouseX: Int, mouseY: Int, delta: Float) { 66 | super.renderBackground(matrices) 67 | DrawableHelper.drawCenteredText(matrices, this.textRenderer, this.title, this.width / 2, 70, 16777215) 68 | this.text.drawCenterWithShadow(matrices, this.width / 2, 90) 69 | super.render(matrices, mouseX, mouseY, delta) 70 | } 71 | 72 | override fun shouldCloseOnEsc(): Boolean { 73 | return false 74 | } 75 | 76 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/deathsgun/modmanager/gui/ModProgressScreen.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 DeathsGun 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package xyz.deathsgun.modmanager.gui 18 | 19 | import kotlinx.coroutines.DelicateCoroutinesApi 20 | import kotlinx.coroutines.GlobalScope 21 | import kotlinx.coroutines.launch 22 | import net.minecraft.client.MinecraftClient 23 | import net.minecraft.client.font.MultilineText 24 | import net.minecraft.client.gui.screen.Screen 25 | import net.minecraft.client.gui.screen.ScreenTexts 26 | import net.minecraft.client.gui.widget.ButtonWidget 27 | import net.minecraft.client.util.math.MatrixStack 28 | import net.minecraft.text.LiteralText 29 | import net.minecraft.text.Text 30 | import net.minecraft.text.TranslatableText 31 | import net.minecraft.util.math.MathHelper 32 | import org.apache.logging.log4j.LogManager 33 | import xyz.deathsgun.modmanager.ModManager 34 | import xyz.deathsgun.modmanager.api.ModInstallResult 35 | import xyz.deathsgun.modmanager.api.ModUpdateResult 36 | import xyz.deathsgun.modmanager.api.mod.Mod 37 | import kotlin.math.roundToInt 38 | 39 | class ModProgressScreen( 40 | val mod: Mod, 41 | private val action: Action, 42 | private val previousScreen: Screen, 43 | private val infoScreen: Screen 44 | ) : 45 | Screen(LiteralText("")) { 46 | 47 | private var status: MultilineText? = null 48 | private lateinit var backButton: ButtonWidget 49 | private var finished: Boolean = false 50 | private var pos = 0 51 | private var rightEnd = 0 52 | private var leftEnd = 0 53 | 54 | @OptIn(DelicateCoroutinesApi::class) 55 | override fun init() { 56 | backButton = addDrawableChild( 57 | ButtonWidget( 58 | width / 2 - 75, 59 | (height * 0.6 + 40).roundToInt(), 60 | 150, 61 | 20, 62 | ScreenTexts.BACK 63 | ) { 64 | client!!.setScreen(previousScreen) 65 | }) 66 | backButton.visible = false 67 | GlobalScope.launch { 68 | when (action) { 69 | Action.UPDATE -> updateMod() 70 | Action.INSTALL -> installMod() 71 | } 72 | } 73 | } 74 | 75 | override fun tick() { 76 | pos += 5 77 | } 78 | 79 | override fun render(matrices: MatrixStack?, mouseX: Int, mouseY: Int, delta: Float) { 80 | rightEnd = width - width / 8 81 | leftEnd = width / 8 82 | super.renderBackground(matrices) 83 | 84 | var y = (height * 0.60).roundToInt() 85 | if (status != null) { 86 | val linesHeight = this.status!!.count() * 9 87 | y = MathHelper.clamp(90 + linesHeight + 12, this.height / 6 + 69, this.height - 24) 88 | status!!.drawCenterWithShadow(matrices, this.width / 2, 90) 89 | } 90 | if (!finished) { 91 | renderProgressBar(matrices, width / 8, y - 5, rightEnd, y + 5) 92 | } 93 | 94 | backButton.y = y + 10 95 | 96 | super.render(matrices, mouseX, mouseY, delta) 97 | } 98 | 99 | private fun renderProgressBar(matrices: MatrixStack?, minX: Int, minY: Int, maxX: Int, maxY: Int) { 100 | val color = 0xFFFFFFFF.toInt() 101 | var barWidth = width / 10 102 | val overlap = (minX + pos + barWidth) - maxX + 2 103 | if (overlap > 0) { 104 | barWidth -= overlap 105 | } 106 | if ((minX + pos) - maxX + 2 > 0) { 107 | pos = 0 108 | } 109 | fill(matrices, minX + 2 + pos, minY + 2, minX + pos + barWidth, maxY - 2, color) 110 | fill(matrices, minX + 1, minY, maxX - 1, minY + 1, color) 111 | fill(matrices, minX + 1, maxY, maxX - 1, maxY - 1, color) 112 | fill(matrices, minX, minY, minX + 1, maxY, color) 113 | fill(matrices, maxX, minY, maxX - 1, maxY, color) 114 | } 115 | 116 | 117 | private fun installMod() { 118 | setStatus(TranslatableText("modmanager.status.installing", mod.name)) 119 | when (val result = ModManager.modManager.update.installMod(mod)) { 120 | is ModInstallResult.Error -> { 121 | LogManager.getLogger().error(result.text.key, result.cause) 122 | client!!.send { 123 | MinecraftClient.getInstance().setScreen(ErrorScreen(previousScreen, infoScreen, result.text)) 124 | } 125 | } 126 | is ModInstallResult.Success -> { 127 | finished = true 128 | this.backButton.visible = true 129 | setStatus(TranslatableText("modmanager.status.install.success", mod.name)) 130 | } 131 | } 132 | } 133 | 134 | private fun updateMod() { 135 | setStatus(TranslatableText("modmanager.status.updating", mod.name)) 136 | val update = ModManager.modManager.update.getUpdateForMod(mod) ?: return 137 | when (val result = ModManager.modManager.update.updateMod(update)) { 138 | is ModUpdateResult.Error -> { 139 | LogManager.getLogger().error(result.text.key, result.cause) 140 | client!!.send { 141 | MinecraftClient.getInstance().setScreen(ErrorScreen(previousScreen, infoScreen, result.text)) 142 | } 143 | } 144 | is ModUpdateResult.Success -> { 145 | finished = true 146 | this.backButton.visible = true 147 | setStatus(TranslatableText("modmanager.status.update.success", mod.name)) 148 | } 149 | } 150 | } 151 | 152 | fun setStatus(text: Text) { 153 | status = MultilineText.create(textRenderer, text, width - 2 * (width / 8)) 154 | } 155 | 156 | override fun onClose() { 157 | client!!.setScreen(previousScreen) 158 | } 159 | 160 | enum class Action { 161 | INSTALL, UPDATE 162 | } 163 | 164 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/deathsgun/modmanager/gui/ModUpdateInfoScreen.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 DeathsGun 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package xyz.deathsgun.modmanager.gui 18 | 19 | import com.mojang.blaze3d.systems.RenderSystem 20 | import com.terraformersmc.modmenu.util.DrawingUtil 21 | import net.minecraft.client.gui.DrawableHelper 22 | import net.minecraft.client.gui.screen.Screen 23 | import net.minecraft.client.gui.screen.ScreenTexts 24 | import net.minecraft.client.gui.widget.ButtonWidget 25 | import net.minecraft.client.util.math.MatrixStack 26 | import net.minecraft.text.* 27 | import xyz.deathsgun.modmanager.ModManager 28 | import xyz.deathsgun.modmanager.api.gui.list.IListScreen 29 | import xyz.deathsgun.modmanager.gui.widget.DescriptionWidget 30 | import xyz.deathsgun.modmanager.update.Update 31 | 32 | 33 | class ModUpdateInfoScreen(private val previousScreen: Screen, private val update: Update) : Screen(LiteralText.EMPTY), 34 | IListScreen { 35 | 36 | private lateinit var descriptionWidget: DescriptionWidget 37 | private lateinit var updateButtonWidget: ButtonWidget 38 | 39 | override fun init() { 40 | descriptionWidget = addSelectableChild( 41 | DescriptionWidget( 42 | client!!, 43 | width - 20, 44 | height - 34, 45 | 79, 46 | height - 30, 47 | textRenderer.fontHeight, 48 | this, 49 | update.version.changelog 50 | ) 51 | ) 52 | descriptionWidget.init() 53 | descriptionWidget.setLeftPos(10) 54 | val buttonX = width / 8 55 | addDrawableChild(ButtonWidget(buttonX, height - 25, 150, 20, ScreenTexts.BACK) { 56 | client?.setScreen(previousScreen) 57 | }) 58 | updateButtonWidget = addDrawableChild(ButtonWidget( 59 | this.width - buttonX - 150, this.height - 25, 150, 20, TranslatableText("modmanager.button.update") 60 | ) { 61 | client!!.setScreen(ModProgressScreen(update.mod, ModProgressScreen.Action.UPDATE, previousScreen, this)) 62 | }) 63 | } 64 | 65 | override fun render(matrices: MatrixStack?, mouseX: Int, mouseY: Int, delta: Float) { 66 | renderBackground(matrices) 67 | descriptionWidget.render(matrices, mouseX, mouseY, delta) 68 | 69 | val iconSize = 64 70 | ModManager.modManager.icons.bindIcon(update.mod) 71 | RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F) 72 | ModManager.modManager.icons.bindIcon(update.mod) 73 | RenderSystem.enableBlend() 74 | DrawableHelper.drawTexture(matrices, 20, 10, 0.0F, 0.0F, iconSize, iconSize, iconSize, iconSize) 75 | RenderSystem.disableBlend() 76 | 77 | val font = client!!.textRenderer 78 | var trimmedTitle: MutableText = LiteralText(font.trimToWidth(update.mod.name, width - 200)) 79 | trimmedTitle = trimmedTitle.setStyle(Style.EMPTY.withBold(true)) 80 | 81 | var detailsY = 15 82 | var textX = 20 + iconSize + 5 83 | 84 | font.draw(matrices, trimmedTitle, textX.toFloat(), detailsY.toFloat(), 0xFFFFFF) 85 | 86 | if (update.mod.author != null) { 87 | detailsY += 12 88 | font.draw( 89 | matrices, 90 | TranslatableText("modmanager.details.author", update.mod.author), 91 | textX.toFloat(), 92 | detailsY.toFloat(), 93 | 0xFFFFFF 94 | ) 95 | } 96 | 97 | detailsY += 12 98 | font.draw( 99 | matrices, 100 | TranslatableText("modmanager.details.versioning", update.installedVersion, update.version.version), 101 | textX.toFloat(), 102 | detailsY.toFloat(), 103 | 0xFFFFFF 104 | ) 105 | 106 | if (update.mod.license != null) { 107 | detailsY += 12 108 | DrawingUtil.drawBadge( 109 | matrices, 110 | textX, 111 | detailsY, 112 | font.getWidth(update.mod.license) + 6, 113 | Text.of(update.mod.license).asOrderedText(), 114 | -0x909396, 115 | -0xcecfd1, 116 | 0xCACACA 117 | ) 118 | } 119 | 120 | for (category in update.mod.categories) { 121 | val textWidth: Int = font.getWidth(category.text) + 6 122 | DrawingUtil.drawBadge( 123 | matrices, 124 | textX, 125 | detailsY + 14, 126 | textWidth, 127 | category.text.asOrderedText(), 128 | -0x909396, 129 | -0xcecfd1, 130 | 0xCACACA 131 | ) 132 | textX += textWidth + 4 133 | } 134 | 135 | super.render(matrices, mouseX, mouseY, delta) 136 | } 137 | 138 | override fun onClose() { 139 | client?.setScreen(previousScreen) 140 | } 141 | 142 | override fun updateSelectedEntry(widget: Any, entry: E?) { 143 | } 144 | 145 | override fun updateMultipleEntries(widget: Any, entries: ArrayList) { 146 | } 147 | 148 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/deathsgun/modmanager/gui/UpdateAllScreen.kt: -------------------------------------------------------------------------------- 1 | package xyz.deathsgun.modmanager.gui 2 | 3 | import kotlinx.coroutines.DelicateCoroutinesApi 4 | import net.minecraft.client.gui.screen.Screen 5 | import net.minecraft.client.gui.screen.ScreenTexts 6 | import net.minecraft.client.gui.widget.ButtonWidget 7 | import net.minecraft.client.util.math.MatrixStack 8 | import net.minecraft.text.TranslatableText 9 | import xyz.deathsgun.modmanager.ModManager 10 | import xyz.deathsgun.modmanager.api.gui.list.IListScreen 11 | import xyz.deathsgun.modmanager.gui.widget.UpdateProgressListWidget 12 | import xyz.deathsgun.modmanager.update.Update 13 | import kotlin.math.min 14 | 15 | class UpdateAllScreen(private val parentScreen: Screen) : Screen(TranslatableText("modmanager.title.updating")), 16 | IListScreen { 17 | 18 | private lateinit var updateList: UpdateProgressListWidget 19 | private lateinit var doneButton: ButtonWidget 20 | private var updated = ArrayList() 21 | 22 | @OptIn(DelicateCoroutinesApi::class) 23 | override fun init() { 24 | updateList = UpdateProgressListWidget( 25 | client!!, 26 | width - 50, 27 | height - 40, 28 | 25, 29 | height - 50, 30 | textRenderer.fontHeight + 4, 31 | this 32 | ) 33 | updateList.setLeftPos(25) 34 | doneButton = addDrawableChild(ButtonWidget(width / 2 - 100, height - 30, 200, 20, ScreenTexts.DONE) { 35 | onClose() 36 | }) 37 | doneButton.active = false 38 | } 39 | 40 | override fun render(matrices: MatrixStack, mouseX: Int, mouseY: Int, delta: Float) { 41 | renderBackground(matrices) 42 | updateList.render(matrices, mouseX, mouseY, delta) 43 | textRenderer.draw(matrices, title, (width / 2 - textRenderer.getWidth(title) / 2).toFloat(), 10F, 0xFFFFFF) 44 | super.render(matrices, mouseX, mouseY, delta) 45 | } 46 | 47 | override fun mouseScrolled(mouseX: Double, mouseY: Double, amount: Double): Boolean { 48 | return updateList.mouseScrolled(mouseX, mouseY, amount) 49 | } 50 | 51 | override fun tick() { 52 | updateList.tick() 53 | val pendingUpdates = getPendingUpdates() 54 | if (pendingUpdates.isEmpty()) { 55 | doneButton.active = true 56 | } 57 | if (pendingUpdates.isEmpty() || !updateList.children().all { it.progress == 1.0 }) { 58 | return 59 | } 60 | for (i in 0..min(1, pendingUpdates.size - 1)) { 61 | val update = pendingUpdates[i] 62 | updated.add(update.mod.id) 63 | updateList.add(update) 64 | updateList.scrollAmount = updateList.maxScroll.toDouble() 65 | } 66 | } 67 | 68 | private fun getPendingUpdates(): List { 69 | return ModManager.modManager.update.getWhitelistedUpdates().filter { 70 | !updated.contains(it.mod.id) 71 | } 72 | } 73 | 74 | override fun onClose() { 75 | client?.setScreen(parentScreen) 76 | } 77 | 78 | override fun updateSelectedEntry(widget: Any, entry: E?) { 79 | } 80 | 81 | override fun updateMultipleEntries(widget: Any, entries: ArrayList) { 82 | } 83 | 84 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/CategoryListEntry.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 DeathsGun 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package xyz.deathsgun.modmanager.gui.widget 18 | 19 | import net.minecraft.client.MinecraftClient 20 | import net.minecraft.client.util.math.MatrixStack 21 | import net.minecraft.text.OrderedText 22 | import net.minecraft.text.Text 23 | import net.minecraft.util.Language 24 | import xyz.deathsgun.modmanager.api.gui.list.MultiSelectListWidget 25 | import xyz.deathsgun.modmanager.api.mod.Category 26 | 27 | 28 | class CategoryListEntry(list: MultiSelectListWidget, val category: Category) : 29 | MultiSelectListWidget.Entry(list, category.id) { 30 | 31 | override fun render( 32 | matrices: MatrixStack?, 33 | index: Int, 34 | y: Int, 35 | x: Int, 36 | entryWidth: Int, 37 | entryHeight: Int, 38 | mouseX: Int, 39 | mouseY: Int, 40 | hovered: Boolean, 41 | tickDelta: Float 42 | ) { 43 | val font = MinecraftClient.getInstance().textRenderer 44 | var text: Text = category.text 45 | if (list.isSelectedEntry(this)) { 46 | text = text.getWithStyle(text.style.withBold(true))[0] 47 | } 48 | val trimmedText: OrderedText = Language.getInstance().reorder(font.trimToWidth(text, entryWidth - 10)) 49 | font.draw(matrices, trimmedText, (x + 3).toFloat(), (y + 1).toFloat(), 0xFFFFFF) 50 | } 51 | 52 | override fun getNarration(): Text { 53 | return category.text 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/CategoryListWidget.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 DeathsGun 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package xyz.deathsgun.modmanager.gui.widget 18 | 19 | import net.minecraft.client.MinecraftClient 20 | import xyz.deathsgun.modmanager.api.gui.list.IListScreen 21 | import xyz.deathsgun.modmanager.api.gui.list.MultiSelectListWidget 22 | import xyz.deathsgun.modmanager.api.mod.Category 23 | 24 | class CategoryListWidget( 25 | client: MinecraftClient, 26 | width: Int, 27 | height: Int, 28 | top: Int, 29 | bottom: Int, 30 | itemHeight: Int, 31 | parent: IListScreen 32 | ) : MultiSelectListWidget(client, width, height, top, bottom, itemHeight, parent) { 33 | 34 | fun addCategories(categories: List) { 35 | categories.forEach { 36 | addEntry(CategoryListEntry(this, it)) 37 | } 38 | } 39 | 40 | fun setSelectedByIndex(index: Int) { 41 | setSelected(getEntry(index)) 42 | } 43 | 44 | fun clear() { 45 | clearEntries() 46 | } 47 | 48 | fun add(category: Category) { 49 | addEntry(CategoryListEntry(this, category)) 50 | } 51 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/DescriptionWidget.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 DeathsGun 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package xyz.deathsgun.modmanager.gui.widget 18 | 19 | import net.minecraft.client.MinecraftClient 20 | import net.minecraft.client.util.math.MatrixStack 21 | import net.minecraft.text.* 22 | import net.minecraft.util.Util 23 | import xyz.deathsgun.modmanager.api.gui.list.IListScreen 24 | import xyz.deathsgun.modmanager.api.gui.list.ListWidget 25 | import xyz.deathsgun.modmanager.md.Markdown 26 | 27 | 28 | class DescriptionWidget( 29 | client: MinecraftClient, 30 | width: Int, 31 | height: Int, 32 | top: Int, 33 | bottom: Int, 34 | itemHeight: Int, 35 | parent: IListScreen, 36 | var text: String 37 | ) : ListWidget(client, width, height, top, bottom, itemHeight, parent) { 38 | 39 | private val textRenderer = MinecraftClient.getInstance().textRenderer 40 | 41 | fun init() { 42 | renderOutline = false 43 | val lines = Markdown(text).toText() 44 | for (line in lines) { 45 | if (textRenderer.getWidth(line) >= width - 10) { 46 | val texts: List = textRenderer.wrapLines(line, width - 10) 47 | for (wrappedLine in texts) { 48 | addEntry(Entry(this, getText(wrappedLine))) 49 | } 50 | continue 51 | } 52 | addEntry(Entry(this, line)) 53 | } 54 | addEntry(Entry(this, LiteralText(""))) 55 | } 56 | 57 | override fun getSelectedOrNull(): Entry? { 58 | return null 59 | } 60 | 61 | override fun getRowWidth(): Int { 62 | return width - 10 63 | } 64 | 65 | override fun getScrollbarPositionX(): Int { 66 | return width - 6 + left 67 | } 68 | 69 | private fun getText(orderedText: OrderedText): LiteralText { 70 | val fields = orderedText.javaClass.declaredFields 71 | var text = "" 72 | var style = Style.EMPTY 73 | for (field in fields) { 74 | field.isAccessible = true 75 | if (field.get(orderedText) is String) { 76 | text = field.get(orderedText) as String 77 | } 78 | if (field.get(orderedText) is Style) { 79 | style = field.get(orderedText) as Style 80 | } 81 | } 82 | return LiteralText(text).apply { this.style = style } 83 | } 84 | 85 | class Entry(list: ListWidget, val text: Text) : ListWidget.Entry(list, text.string) { 86 | 87 | private val textRenderer = MinecraftClient.getInstance().textRenderer 88 | var x: Int = 0 89 | 90 | override fun render( 91 | matrices: MatrixStack?, 92 | index: Int, 93 | y: Int, 94 | x: Int, 95 | entryWidth: Int, 96 | entryHeight: Int, 97 | mouseX: Int, 98 | mouseY: Int, 99 | hovered: Boolean, 100 | tickDelta: Float 101 | ) { 102 | if (y >= list.bottom - textRenderer.fontHeight + 2) { 103 | return 104 | } 105 | textRenderer.draw(matrices, text, x.toFloat(), y.toFloat(), 0xFFFFFF) 106 | } 107 | 108 | override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean { 109 | if (isMouseOver(mouseX, mouseY)) { 110 | val event = text.style.clickEvent 111 | if (event == null || event.action != ClickEvent.Action.OPEN_URL) { 112 | return super.mouseClicked(mouseX, mouseY, button) 113 | } 114 | Util.getOperatingSystem().open(event.value) 115 | } 116 | return super.mouseClicked(mouseX, mouseY, button) 117 | } 118 | 119 | override fun isMouseOver(mouseX: Double, mouseY: Double): Boolean { 120 | return super.isMouseOver(mouseX, mouseY) && x + textRenderer.getWidth(text) >= mouseX 121 | } 122 | 123 | override fun getNarration(): Text { 124 | return text 125 | } 126 | } 127 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/ModListEntry.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 DeathsGun 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package xyz.deathsgun.modmanager.gui.widget 18 | 19 | import com.mojang.blaze3d.systems.RenderSystem 20 | import com.terraformersmc.modmenu.util.DrawingUtil 21 | import net.minecraft.client.MinecraftClient 22 | import net.minecraft.client.gui.DrawableHelper 23 | import net.minecraft.client.util.math.MatrixStack 24 | import net.minecraft.text.* 25 | import net.minecraft.util.Language 26 | import xyz.deathsgun.modmanager.ModManager 27 | import xyz.deathsgun.modmanager.api.gui.list.ListWidget 28 | import xyz.deathsgun.modmanager.api.mod.Mod 29 | import xyz.deathsgun.modmanager.api.mod.State 30 | 31 | 32 | class ModListEntry(private val client: MinecraftClient, override val list: ModListWidget, val mod: Mod) : 33 | ListWidget.Entry(list, mod.id) { 34 | 35 | private val state = ModManager.modManager.getModState(mod.id) 36 | 37 | override fun render( 38 | matrices: MatrixStack?, index: Int, y: Int, x: Int, entryWidth: Int, entryHeight: Int, 39 | mouseX: Int, mouseY: Int, hovered: Boolean, tickDelta: Float 40 | ) { 41 | val iconSize = 32 42 | RenderSystem.setShaderColor(1.0f, 1.0f, 1.0f, 1.0f) 43 | ModManager.modManager.icons.bindIcon(mod) 44 | RenderSystem.enableBlend() 45 | DrawableHelper.drawTexture(matrices, x, y, 0.0f, 0.0f, iconSize, iconSize, iconSize, iconSize) 46 | RenderSystem.disableBlend() 47 | val name: Text = LiteralText(mod.name) 48 | var trimmedName: StringVisitable = name 49 | var maxNameWidth = entryWidth - iconSize - 3 50 | val font = this.client.textRenderer 51 | var primaryColor = 0xFFFFFF 52 | var secondaryColor = 0xFFFFFF 53 | var badgeText: OrderedText? = null 54 | if (state == State.INSTALLED) { 55 | primaryColor = 0xff0e2a55.toInt() 56 | secondaryColor = 0xff2b4b7c.toInt() 57 | badgeText = TranslatableText("modmanager.badge.installed").asOrderedText() 58 | maxNameWidth -= font.getWidth(badgeText) + 6 59 | } else if (state == State.OUTDATED) { 60 | primaryColor = 0xff530C17.toInt() 61 | secondaryColor = 0xff841426.toInt() 62 | badgeText = TranslatableText("modmanager.badge.outdated").asOrderedText() 63 | maxNameWidth -= font.getWidth(badgeText) + 6 64 | } 65 | 66 | val textWidth = font.getWidth(name) 67 | if (textWidth > maxNameWidth) { 68 | val ellipsis = StringVisitable.plain("...") 69 | trimmedName = 70 | StringVisitable.concat(font.trimToWidth(name, maxNameWidth - font.getWidth(ellipsis)), ellipsis) 71 | } 72 | font.draw( 73 | matrices, 74 | Language.getInstance().reorder(trimmedName), 75 | (x + iconSize + 3).toFloat(), 76 | (y + 1).toFloat(), 77 | 0xFFFFFF 78 | ) 79 | if (badgeText != null) { 80 | DrawingUtil.drawBadge( 81 | matrices, 82 | x + iconSize + 3 + textWidth + 3, 83 | y + 1, 84 | font.getWidth(badgeText) + 6, 85 | badgeText, 86 | secondaryColor, 87 | primaryColor, 88 | 0xFFFFFF 89 | ) 90 | } 91 | 92 | DrawingUtil.drawWrappedString( 93 | matrices, 94 | mod.shortDescription, 95 | (x + iconSize + 3 + 4), 96 | (y + client.textRenderer.fontHeight + 4), 97 | entryWidth - iconSize - 7, 98 | 2, 99 | 0x808080 100 | ) 101 | } 102 | 103 | override fun getNarration(): Text { 104 | return LiteralText(mod.name) 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/ModListWidget.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 DeathsGun 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package xyz.deathsgun.modmanager.gui.widget 18 | 19 | import net.minecraft.client.MinecraftClient 20 | import xyz.deathsgun.modmanager.api.gui.list.IListScreen 21 | import xyz.deathsgun.modmanager.api.gui.list.ListWidget 22 | import xyz.deathsgun.modmanager.api.mod.Mod 23 | 24 | class ModListWidget( 25 | client: MinecraftClient, 26 | width: Int, 27 | height: Int, 28 | top: Int, 29 | bottom: Int, 30 | itemHeight: Int, 31 | parent: IListScreen 32 | ) : ListWidget(client, width, height, top, bottom, itemHeight, parent) { 33 | 34 | fun setMods(mods: List) { 35 | this.clearEntries() 36 | mods.forEach { 37 | this.addEntry(ModListEntry(client, this, it)) 38 | } 39 | } 40 | 41 | fun clear() { 42 | this.clearEntries() 43 | } 44 | 45 | fun add(mod: Mod) { 46 | this.addEntry(ModListEntry(client, this, mod)) 47 | } 48 | 49 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/TexturedButton.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 DeathsGun 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package xyz.deathsgun.modmanager.gui.widget 18 | 19 | import com.mojang.blaze3d.systems.RenderSystem 20 | import com.terraformersmc.modmenu.gui.widget.ModMenuTexturedButtonWidget 21 | import net.minecraft.client.util.math.MatrixStack 22 | import net.minecraft.text.LiteralText 23 | import net.minecraft.util.Identifier 24 | 25 | class TexturedButton( 26 | x: Int, 27 | y: Int, 28 | width: Int, 29 | height: Int, 30 | private val u: Int, 31 | private val v: Int, 32 | texture: Identifier, 33 | private val uWidth: Int, 34 | private val vHeight: Int, 35 | onPress: PressAction?, 36 | tooltipSupplier: TooltipSupplier 37 | ) : ModMenuTexturedButtonWidget( 38 | x, 39 | y, 40 | width, 41 | height, 42 | u, 43 | v, 44 | texture, 45 | uWidth, 46 | vHeight, 47 | onPress, 48 | LiteralText.EMPTY, 49 | tooltipSupplier 50 | ) { 51 | 52 | var image: Identifier = texture 53 | 54 | override fun renderButton(matrices: MatrixStack, mouseX: Int, mouseY: Int, delta: Float) { 55 | RenderSystem.setShaderColor(1f, 1f, 1f, 1f) 56 | RenderSystem.setShaderTexture(0, image) 57 | RenderSystem.disableDepthTest() 58 | var adjustedV = v 59 | if (!active) { 60 | adjustedV += height * 2 61 | } else if (this.isHovered) { 62 | adjustedV += height 63 | } 64 | drawTexture( 65 | matrices, 66 | x, y, u.toFloat(), adjustedV.toFloat(), width, height, uWidth, vHeight 67 | ) 68 | RenderSystem.enableDepthTest() 69 | if (this.isHovered) { 70 | renderTooltip(matrices, mouseX, mouseY) 71 | } 72 | } 73 | 74 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/UpdateProgressListEntry.kt: -------------------------------------------------------------------------------- 1 | package xyz.deathsgun.modmanager.gui.widget 2 | 3 | import kotlinx.coroutines.DelicateCoroutinesApi 4 | import kotlinx.coroutines.GlobalScope 5 | import kotlinx.coroutines.delay 6 | import kotlinx.coroutines.launch 7 | import net.minecraft.client.MinecraftClient 8 | import net.minecraft.client.gui.DrawableHelper 9 | import net.minecraft.client.gui.screen.ScreenTexts 10 | import net.minecraft.client.util.math.MatrixStack 11 | import net.minecraft.text.LiteralText 12 | import net.minecraft.text.Text 13 | import xyz.deathsgun.modmanager.ModManager 14 | import xyz.deathsgun.modmanager.api.gui.list.ListWidget 15 | import xyz.deathsgun.modmanager.update.ProgressListener 16 | import xyz.deathsgun.modmanager.update.Update 17 | 18 | @OptIn(DelicateCoroutinesApi::class) 19 | class UpdateProgressListEntry(list: ListWidget, val update: Update) : 20 | ListWidget.Entry(list, update.mod.id), ProgressListener { 21 | 22 | internal var progress = 0.0 23 | private var pos = 0 24 | 25 | init { 26 | GlobalScope.launch { 27 | delay(200) 28 | ModManager.modManager.update.updateMod(update) { this@UpdateProgressListEntry.progress = it } 29 | } 30 | } 31 | 32 | override fun render( 33 | matrices: MatrixStack, 34 | index: Int, 35 | y: Int, 36 | x: Int, 37 | entryWidth: Int, 38 | entryHeight: Int, 39 | mouseX: Int, 40 | mouseY: Int, 41 | hovered: Boolean, 42 | tickDelta: Float 43 | ) { 44 | val textRenderer = MinecraftClient.getInstance().textRenderer 45 | val infoText = "${update.mod.name} v${update.installedVersion} to ${update.version.version}" 46 | textRenderer.draw(matrices, infoText, x.toFloat(), y + 1f, 0xFFFFFF) 47 | val infoTextWidth = textRenderer.getWidth(infoText) + 5 48 | if (progress == 1.0) { 49 | textRenderer.draw(matrices, ScreenTexts.DONE, (x + entryWidth - textRenderer.getWidth(ScreenTexts.DONE)).toFloat(), y + 1f, 0xFFFFFF) 50 | return 51 | } 52 | renderProgressBar(matrices, entryWidth - infoTextWidth, x + infoTextWidth, y, x + entryWidth, y + entryHeight) 53 | } 54 | 55 | fun tick() { 56 | pos += 5 57 | } 58 | 59 | override fun onProgress(progress: Double) { 60 | this.progress = progress 61 | } 62 | 63 | private fun renderProgressBar(matrices: MatrixStack, width: Int, minX: Int, minY: Int, maxX: Int, maxY: Int) { 64 | val color = 0xFFFFFFFF.toInt() 65 | var barWidth = width / 10 66 | val overlap = (minX + pos + barWidth) - maxX + 2 67 | if (overlap > 0) { 68 | barWidth -= overlap 69 | } 70 | if ((minX + pos) - maxX + 2 > 0) { 71 | pos = 0 72 | } 73 | DrawableHelper.fill(matrices, minX + 2 + pos, minY + 2, minX + pos + barWidth, maxY - 2, color) 74 | DrawableHelper.fill(matrices, minX + 1, minY, maxX - 1, minY + 1, color) 75 | DrawableHelper.fill(matrices, minX + 1, maxY, maxX - 1, maxY - 1, color) 76 | DrawableHelper.fill(matrices, minX, minY, minX + 1, maxY, color) 77 | DrawableHelper.fill(matrices, maxX, minY, maxX - 1, maxY, color) 78 | } 79 | 80 | override fun getNarration(): Text { 81 | return LiteralText.EMPTY 82 | } 83 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/UpdateProgressListWidget.kt: -------------------------------------------------------------------------------- 1 | package xyz.deathsgun.modmanager.gui.widget 2 | 3 | import net.minecraft.client.MinecraftClient 4 | import xyz.deathsgun.modmanager.api.gui.list.IListScreen 5 | import xyz.deathsgun.modmanager.api.gui.list.ListWidget 6 | import xyz.deathsgun.modmanager.update.Update 7 | 8 | class UpdateProgressListWidget( 9 | client: MinecraftClient, 10 | width: Int, 11 | height: Int, 12 | top: Int, 13 | bottom: Int, 14 | itemHeight: Int, 15 | parent: IListScreen 16 | ) : ListWidget(client, width, height, top, bottom, itemHeight, parent) { 17 | 18 | override fun isSelectedEntry(entry: Entry): Boolean { 19 | return false 20 | } 21 | 22 | override fun isSelectedEntry(index: Int): Boolean { 23 | return false 24 | } 25 | 26 | fun tick() { 27 | for (child in children()) { 28 | child.tick() 29 | } 30 | } 31 | 32 | fun add(update: Update) { 33 | addEntry(UpdateProgressListEntry(this, update)) 34 | } 35 | 36 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/deathsgun/modmanager/icon/IconCache.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 DeathsGun 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package xyz.deathsgun.modmanager.icon 18 | 19 | import com.mojang.blaze3d.systems.RenderSystem 20 | import kotlinx.coroutines.DelicateCoroutinesApi 21 | import kotlinx.coroutines.GlobalScope 22 | import kotlinx.coroutines.launch 23 | import net.fabricmc.loader.api.FabricLoader 24 | import net.minecraft.client.MinecraftClient 25 | import net.minecraft.client.texture.NativeImage 26 | import net.minecraft.client.texture.NativeImageBackedTexture 27 | import net.minecraft.util.Identifier 28 | import org.apache.commons.io.FileUtils 29 | import org.apache.logging.log4j.LogManager 30 | import xyz.deathsgun.modmanager.ModManager 31 | import xyz.deathsgun.modmanager.api.mod.Mod 32 | import java.net.URI 33 | import java.net.http.HttpClient 34 | import java.net.http.HttpRequest 35 | import java.net.http.HttpResponse 36 | import java.nio.file.Files 37 | import java.util.stream.Collectors 38 | 39 | class IconCache { 40 | 41 | private val logger = LogManager.getLogger("IconCache") 42 | private val unknownIcon = Identifier("textures/misc/unknown_pack.png") 43 | private val loadingIcon = Identifier("modmanager", "textures/gui/loading.png") 44 | private val iconsDir = FabricLoader.getInstance().gameDir.resolve(".icons") 45 | private val state = HashMap() 46 | private val http = HttpClient.newHttpClient() 47 | 48 | init { 49 | Files.createDirectories(iconsDir) 50 | } 51 | 52 | @OptIn(DelicateCoroutinesApi::class) 53 | fun bindIcon(mod: Mod) { 54 | val icon = when (this.state[mod.id] ?: IconState.NOT_FOUND) { 55 | IconState.NOT_FOUND -> { 56 | GlobalScope.launch { 57 | downloadIcon(mod) 58 | } 59 | loadingIcon 60 | } 61 | IconState.DOWNLOADED -> { 62 | readIcon(mod) 63 | } 64 | IconState.LOADED -> { 65 | Identifier("modmanager", "mod_icons/${mod.id.lowercase()}") 66 | } 67 | IconState.DOWNLOADING -> loadingIcon 68 | IconState.ERRORED -> unknownIcon 69 | } 70 | RenderSystem.setShaderTexture(0, icon) 71 | } 72 | 73 | private fun readIcon(mod: Mod): Identifier { 74 | return try { 75 | val icon = Identifier("modmanager", "mod_icons/${mod.id.lowercase()}") 76 | val image = NativeImage.read(Files.newInputStream(iconsDir.resolve(mod.id))) 77 | MinecraftClient.getInstance().textureManager.registerTexture(icon, NativeImageBackedTexture(image)) 78 | this.state[mod.id] = IconState.LOADED 79 | icon 80 | } catch (e: Exception) { 81 | this.state[mod.id] = IconState.ERRORED 82 | logger.error("Error while loading icon for {}: {}", mod.slug, e.message) 83 | unknownIcon 84 | } 85 | } 86 | 87 | private fun downloadIcon(mod: Mod) { 88 | if (mod.iconUrl == null) { 89 | state[mod.id] = IconState.ERRORED 90 | return 91 | } 92 | val iconState = state[mod.id] ?: IconState.NOT_FOUND 93 | if (iconState != IconState.NOT_FOUND) { 94 | return 95 | } 96 | state[mod.id] = IconState.DOWNLOADING 97 | try { 98 | val request = HttpRequest.newBuilder(URI.create(mod.iconUrl)).GET() 99 | .setHeader("User-Agent", "ModMenu " + ModManager.getVersion()).build() 100 | val response = http.send(request, HttpResponse.BodyHandlers.ofByteArray()) 101 | if (response.statusCode() != 200) { 102 | state[mod.id] = IconState.ERRORED 103 | return 104 | } 105 | Files.write(iconsDir.resolve(mod.id), response.body()) 106 | state[mod.id] = IconState.DOWNLOADED 107 | } catch (e: Exception) { 108 | state[mod.id] = IconState.ERRORED 109 | logger.error("Error while downloading icon for {}: {}", mod.slug, e.message) 110 | } 111 | } 112 | 113 | fun destroyAll() { 114 | for ((mod, state) in state) { 115 | if (state != IconState.LOADED) { 116 | continue 117 | } 118 | val icon = Identifier("modmanager", "mod_icons/${mod.lowercase()}") 119 | MinecraftClient.getInstance().textureManager.destroyTexture(icon) 120 | this.state[mod] = IconState.DOWNLOADED 121 | } 122 | } 123 | 124 | fun cleanupCache() { 125 | logger.info("Starting cleanup...") 126 | var files = Files.list(iconsDir) 127 | .sorted { o1, o2 -> 128 | o1.toFile().lastModified().compareTo(o2.toFile().lastModified()) 129 | }.collect(Collectors.toList()) 130 | if (files.isEmpty()) { 131 | return 132 | } 133 | while (FileUtils.sizeOfDirectory(iconsDir.toFile()) >= 10000000) { 134 | if (files.isEmpty()) { 135 | return 136 | } 137 | Files.delete(files[0]) 138 | files = Files.list(iconsDir) 139 | .sorted { o1, o2 -> 140 | o1.toFile().lastModified().compareTo(o2.toFile().lastModified()) 141 | }.collect(Collectors.toList()) 142 | } 143 | logger.info("Cleanup done!") 144 | } 145 | 146 | private enum class IconState { 147 | NOT_FOUND, DOWNLOADING, DOWNLOADED, LOADED, ERRORED 148 | } 149 | 150 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/deathsgun/modmanager/md/Markdown.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 DeathsGun 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package xyz.deathsgun.modmanager.md 18 | 19 | import net.minecraft.text.ClickEvent 20 | import net.minecraft.text.LiteralText 21 | import net.minecraft.text.MutableText 22 | import net.minecraft.util.Formatting 23 | import java.util.regex.Matcher 24 | import java.util.regex.Pattern 25 | 26 | 27 | class Markdown(private var text: String) { 28 | 29 | private val boldPattern: Pattern = Pattern.compile("\\*\\*(.*?)\\*\\*") 30 | private val linkPattern: Pattern = Pattern.compile("\\[(.*?)]\\((.*?)\\)") 31 | private val imagePattern: Pattern = Pattern.compile("!\\[(.*?)]\\((.*?)\\)") 32 | 33 | fun toText(): List { 34 | text = text.replace("\u00A0", " ") 35 | 36 | text = text.replace("\r".toRegex(), "") 37 | text = text.replace("
".toRegex(), "\n").replace("
".toRegex(), "\n") 38 | val lines = text.split("\n").toTypedArray() 39 | val texts = ArrayList() 40 | for (line in lines) { 41 | if (imagePattern.matcher(line).find()) { 42 | continue 43 | } 44 | texts.add(processLine(line)) 45 | } 46 | return texts 47 | } 48 | 49 | private fun processLine(text: String): MutableText { 50 | if (boldPattern.matcher(text).find()) { 51 | return extractBoldText(text) 52 | } 53 | return if (linkPattern.matcher(text).find()) { 54 | extractLinkText(text) 55 | } else LiteralText(text) 56 | } 57 | 58 | private fun extractLinkText(text: String): MutableText { 59 | val matcher: Matcher = linkPattern.matcher(text) 60 | if (!matcher.find()) { 61 | return LiteralText(text) 62 | } 63 | val linkText: String = matcher.group(1) 64 | val begin = text.indexOf(linkText) 65 | val preText = LiteralText(text.substring(0, begin).replace("\\[".toRegex(), "")) 66 | val matchedText = LiteralText(linkText).formatted(Formatting.UNDERLINE, Formatting.BLUE) 67 | matchedText.style = matchedText.style.withClickEvent(ClickEvent(ClickEvent.Action.OPEN_URL, matcher.group(2))) 68 | return preText.append(matchedText) 69 | .append(extractLinkText(text.substring(begin + 3 + linkText.length + matcher.group(2).length))) 70 | } 71 | 72 | private fun extractBoldText(text: String): MutableText { 73 | val matcher: Matcher = boldPattern.matcher(text) 74 | if (!matcher.find()) { 75 | return LiteralText(text) 76 | } 77 | val boldText: String = matcher.group(1) 78 | val begin = text.indexOf(boldText) 79 | val preText = LiteralText(text.substring(0, begin).replace("\\*\\*".toRegex(), "")) 80 | val matchedText = LiteralText(boldText).formatted(Formatting.BOLD) 81 | return preText.append(matchedText).append(extractBoldText(text.substring(begin + 2 + boldText.length))) 82 | } 83 | 84 | 85 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/deathsgun/modmanager/models/FabricMetadata.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 DeathsGun 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package xyz.deathsgun.modmanager.models 18 | 19 | import kotlinx.serialization.Serializable 20 | 21 | @Serializable 22 | data class FabricMetadata( 23 | val id: String, 24 | val name: String, 25 | val custom: Custom = Custom(emptyMap()) 26 | ) { 27 | @Serializable 28 | data class Custom( 29 | val modmanager: Map = emptyMap() 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/models/DetailedMod.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 DeathsGun 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package xyz.deathsgun.modmanager.providers.modrinth.models 18 | 19 | import kotlinx.serialization.SerialName 20 | import kotlinx.serialization.Serializable 21 | 22 | @Serializable 23 | data class DetailedMod( 24 | val id: String, 25 | val slug: String, 26 | val title: String, 27 | val description: String, 28 | val body: String, 29 | val license: License, 30 | val categories: List, 31 | @SerialName("icon_url") 32 | val iconUrl: String? 33 | ) { 34 | @Serializable 35 | data class License(val name: String) 36 | } 37 | -------------------------------------------------------------------------------- /src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/models/ModResult.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 DeathsGun 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package xyz.deathsgun.modmanager.providers.modrinth.models 18 | 19 | import kotlinx.serialization.SerialName 20 | import kotlinx.serialization.Serializable 21 | 22 | @Serializable 23 | data class ModResult( 24 | @SerialName("mod_id") 25 | val id: String, 26 | val slug: String, 27 | val author: String, 28 | val title: String, 29 | val description: String, 30 | @SerialName("icon_url") 31 | val iconUrl: String?, 32 | val categories: ArrayList 33 | ) 34 | -------------------------------------------------------------------------------- /src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/models/ModrinthVersion.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 DeathsGun 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package xyz.deathsgun.modmanager.providers.modrinth.models 18 | 19 | import kotlinx.serialization.SerialName 20 | import kotlinx.serialization.Serializable 21 | 22 | @Serializable 23 | data class ModrinthVersion( 24 | @SerialName("version_number") 25 | val version: String, 26 | val changelog: String, 27 | @SerialName("date_published") 28 | val releaseDate: String, 29 | @SerialName("version_type") 30 | val type: String, 31 | @SerialName("game_versions") 32 | val gameVersions: List, 33 | val files: List, 34 | val loaders: List, 35 | ) { 36 | @Serializable 37 | data class File( 38 | val hashes: Map, 39 | val filename: String, 40 | val url: String, 41 | val primary: Boolean 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/models/SearchResult.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 DeathsGun 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package xyz.deathsgun.modmanager.providers.modrinth.models 18 | 19 | import kotlinx.serialization.Serializable 20 | import net.minecraft.text.TranslatableText 21 | import xyz.deathsgun.modmanager.api.mod.Category 22 | import xyz.deathsgun.modmanager.api.mod.Mod 23 | 24 | @Serializable 25 | data class SearchResult( 26 | val hits: List 27 | ) { 28 | fun toList(): List { 29 | val mods = ArrayList() 30 | for (mod in hits) { 31 | val categoriesList = ArrayList() 32 | mod.categories.forEach { categoryId -> 33 | categoriesList.add( 34 | Category( 35 | categoryId, 36 | TranslatableText("modmanager.category.${categoryId}") 37 | ) 38 | ) 39 | } 40 | mods.add( 41 | Mod( 42 | mod.id.replaceFirst("local-", ""), 43 | mod.slug, 44 | mod.author, 45 | mod.title, 46 | mod.description, 47 | mod.iconUrl, 48 | null, 49 | null, 50 | categoriesList, 51 | ) 52 | ) 53 | } 54 | return mods 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/kotlin/xyz/deathsgun/modmanager/state/SavedState.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 DeathsGun 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package xyz.deathsgun.modmanager.state 18 | 19 | import xyz.deathsgun.modmanager.api.mod.State 20 | 21 | data class SavedState( 22 | val fabricId: String, 23 | val modId: String, 24 | val state: State 25 | ) 26 | -------------------------------------------------------------------------------- /src/main/kotlin/xyz/deathsgun/modmanager/update/ProgressListener.kt: -------------------------------------------------------------------------------- 1 | package xyz.deathsgun.modmanager.update 2 | 3 | interface ProgressListener { 4 | 5 | fun onProgress(progress: Double) 6 | 7 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/deathsgun/modmanager/update/Update.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 DeathsGun 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package xyz.deathsgun.modmanager.update 18 | 19 | import xyz.deathsgun.modmanager.api.mod.Mod 20 | import xyz.deathsgun.modmanager.api.mod.Version 21 | 22 | data class Update(val mod: Mod, val fabricId: String, val installedVersion: String, val version: Version) 23 | -------------------------------------------------------------------------------- /src/main/kotlin/xyz/deathsgun/modmanager/update/VersionFinder.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 DeathsGun 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package xyz.deathsgun.modmanager.update 18 | 19 | import net.fabricmc.loader.api.SemanticVersion 20 | import net.fabricmc.loader.api.VersionParsingException 21 | import xyz.deathsgun.modmanager.api.mod.Version 22 | import xyz.deathsgun.modmanager.config.Config 23 | 24 | object VersionFinder { 25 | 26 | fun findUpdateFallback( 27 | installedVersion: String, 28 | mcReleaseTarget: String, 29 | mcVersion: String, 30 | updateChannel: Config.UpdateChannel, 31 | modVersions: List 32 | ): Version? { 33 | val versions = 34 | modVersions.filter { updateChannel.isReleaseAllowed(it.type) } 35 | .filter { it.gameVersions.any { it1 -> it1.startsWith(mcVersion) || it1.startsWith(mcReleaseTarget) } } 36 | .sortedByDescending { it.releaseDate } 37 | 38 | val version = versions.firstOrNull() 39 | if (version?.version == installedVersion) { 40 | return null 41 | } 42 | return version 43 | } 44 | 45 | internal fun findUpdateByVersion( 46 | installedVersion: String, 47 | mcReleaseTarget: String, 48 | mcVersion: String, 49 | channel: Config.UpdateChannel, 50 | modVersions: List 51 | ): Version? { 52 | val versions = modVersions.filter { channel.isReleaseAllowed(it.type) } 53 | .filter { it.gameVersions.any { it1 -> it1.startsWith(mcVersion) || it1.startsWith(mcReleaseTarget) } } 54 | var latestVersion: Version? = null 55 | var latestVer: SemanticVersion? = null 56 | val installedVer = SemanticVersion.parse(installedVersion) 57 | for (version in versions) { 58 | val parsedVersion = SemanticVersion.parse(version.version) 59 | if (latestVersion == null) { 60 | latestVersion = version 61 | latestVer = parsedVersion 62 | continue 63 | } 64 | if (latestVer != null && parsedVersion > latestVer) { 65 | latestVersion = version 66 | latestVer = parsedVersion 67 | } 68 | } 69 | if (installedVersion == latestVersion?.version || (latestVer != null && installedVer >= latestVer)) { 70 | return null 71 | } 72 | return latestVersion 73 | } 74 | 75 | fun findUpdate( 76 | installedVersion: String, 77 | mcReleaseTarget: String, 78 | mcVersion: String, 79 | channel: Config.UpdateChannel, 80 | modVersions: List 81 | ): Version? { 82 | return try { 83 | findUpdateByVersion(installedVersion, mcReleaseTarget, mcVersion, channel, modVersions) 84 | } catch (e: VersionParsingException) { 85 | findUpdateFallback(installedVersion, mcReleaseTarget, mcVersion, channel, modVersions) 86 | } 87 | } 88 | } -------------------------------------------------------------------------------- /src/main/resources/assets/modmanager/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModManagerMC/ModManager/c6cd9535504837a97fef3b73d43df3a6c95a2ca3/src/main/resources/assets/modmanager/icon.png -------------------------------------------------------------------------------- /src/main/resources/assets/modmanager/lang/en_us.json: -------------------------------------------------------------------------------- 1 | { 2 | "modmanager.badge.installed": "Installed", 3 | "modmanager.badge.outdated": "Outdated", 4 | "modmanager.button.update": "Update", 5 | "modmanager.button.install": "Install", 6 | "modmanager.button.remove": "Remove", 7 | "modmanager.button.updateAll": "Update all", 8 | "modmanager.button.tryAgain": "Try again", 9 | "modmanager.button.defaultProvider": "Default provider", 10 | "modmanager.button.updateChannel": "Update channel", 11 | "modmanager.button.save": "Save", 12 | "modmanager.button.hide": "Hide updates", 13 | "modmanager.button.show": "Show updates", 14 | "modmanager.button.open": "Open ModManager", 15 | "modmanager.categories": "Categories", 16 | "modmanager.category.updatable": "Updatable mods", 17 | "modmanager.category.technology": "Technology", 18 | "modmanager.category.adventure": "Adventure", 19 | "modmanager.category.magic": "Magic", 20 | "modmanager.category.utility": "Utility", 21 | "modmanager.category.decoration": "Decoration", 22 | "modmanager.category.library": "Library", 23 | "modmanager.category.cursed": "Cursed", 24 | "modmanager.category.worldgen": "World Gen", 25 | "modmanager.category.storage": "Storage", 26 | "modmanager.category.search": "Search", 27 | "modmanager.category.food": "Food", 28 | "modmanager.category.equipment": "Equipment", 29 | "modmanager.category.misc": "Miscellaneous", 30 | "modmanager.changes.title": "§e§lChanges detected§r", 31 | "modmanager.changes.message": "Changes will take effect after a relaunch.\nQuit Minecraft now?", 32 | "modmanager.channel.info": "Switch between beta and stable versions", 33 | "modmanager.channel.all": "All", 34 | "modmanager.channel.stable": "Stable", 35 | "modmanager.channel.unstable": "Unstable", 36 | "modmanager.details.author": "By %s", 37 | "modmanager.details.versioning": "From %s to %s", 38 | "modmanager.error.title": "§b§4Error:§r", 39 | "modmanager.error.container.notFound": "The mod you're trying to update has been unloaded in the meantime\nThis shouldn't be possible", 40 | "modmanager.error.unknown": "Unknown error while retrieving data:\n %s", 41 | "modmanager.error.unknown.install": "Unknown error during the installation process:\n %s", 42 | "modmanager.error.unknown.update": "Unknown error during the update process:\n %s", 43 | "modmanager.error.invalidStatus": "Received invalid status code:\n %d", 44 | "modmanager.error.network": "Network error while retrieving data:\n %s", 45 | "modmanager.error.failedToParse": "Failed to parse response:\n %s", 46 | "modmanager.error.noCompatibleModVersionFound": "No compatible version found for your current channel!\nTry changing the channel in the Mod Manager settings!", 47 | "modmanager.error.noProviderSelected": "The selected provider %s is not available!", 48 | "modmanager.error.update.noFabricJar": "The mod author has marked a version compatible\nto fabric but does not provide a for it!", 49 | "modmanager.error.jar.notFound": "Error, jar file not found. Is it part of another mod?", 50 | "modmanager.error.jar.failedDelete": "Failed to delete mod:\n %s", 51 | "modmanager.error.invalidHash": "Error, the downloaded file is probably broken as the %s hashes don't match\nTry checking your network or contact the mod author", 52 | "modmanager.status.installing": "Installing %s...", 53 | "modmanager.status.install.success": "Successfully installed %s!\nDo you want to go back?", 54 | "modmanager.status.updating": "Updating %s...", 55 | "modmanager.status.update.success": "Successfully updated %s!\nDo you want to go back?", 56 | "modmanager.sorting.sort": "Sort by", 57 | "modmanager.sorting.relevance": "Relevance", 58 | "modmanager.sorting.downloads": "Downloads", 59 | "modmanager.sorting.updated": "Updated", 60 | "modmanager.sorting.newest": "Newest", 61 | "modmanager.search": "Search...", 62 | "modmanager.page.next": "Next Page", 63 | "modmanager.page.previous": "Previous Page", 64 | "modmanager.provider.info": "Allows you to change the default provider for browsing", 65 | "modmanager.toast.update.title": "Updates Available!", 66 | "modmanager.toast.update.description": "%d mods can be updated", 67 | "modmanager.title.updating": "Updating all mods..." 68 | } -------------------------------------------------------------------------------- /src/main/resources/assets/modmanager/lang/es_ar.json: -------------------------------------------------------------------------------- 1 | { 2 | "modmanager.badge.installed": "Instalado", 3 | "modmanager.badge.outdated": "Obsoleto", 4 | "modmanager.button.update": "Actualizar", 5 | "modmanager.button.install": "Instalar", 6 | "modmanager.button.remove": "Desinstalar", 7 | "modmanager.button.updateAll": "Actualizar todo", 8 | "modmanager.button.tryAgain": "Reintentar", 9 | "modmanager.button.defaultProvider": "Proveedor predeterminado", 10 | "modmanager.button.updateChannel": "Canal de actualizaciones", 11 | "modmanager.button.save": "Guardar", 12 | "modmanager.button.hide": "Ocultar actualizaciones", 13 | "modmanager.button.show": "Mostrar actualizaciones", 14 | "modmanager.button.open": "Abrir ModManager", 15 | "modmanager.categories": "Categorías", 16 | "modmanager.category.updatable": "Mods con actualizaciones", 17 | "modmanager.category.technology": "Tecnología", 18 | "modmanager.category.adventure": "Aventura", 19 | "modmanager.category.magic": "Magia", 20 | "modmanager.category.utility": "Utilidad", 21 | "modmanager.category.decoration": "Decoración", 22 | "modmanager.category.library": "Librería", 23 | "modmanager.category.cursed": "Cursed", 24 | "modmanager.category.worldgen": "Gen. de Mundo", 25 | "modmanager.category.storage": "Almacenaje", 26 | "modmanager.category.search": "Buscar", 27 | "modmanager.category.food": "Comida", 28 | "modmanager.category.equipment": "Equipamento", 29 | "modmanager.category.misc": "Misceláneo", 30 | "modmanager.changes.title": "§e§lSe han detectado cambios§r", 31 | "modmanager.changes.message": "Los cambios surtirán efecto tras reiniciar.\n¿Desear salir de Minecraft ahora?", 32 | "modmanager.channel.info": "Cambia entre las versiones beta y estables", 33 | "modmanager.channel.all": "Todo", 34 | "modmanager.channel.stable": "Estable", 35 | "modmanager.channel.unstable": "Inestable", 36 | "modmanager.details.author": "Por %s", 37 | "modmanager.details.versioning": "De %s a %s", 38 | "modmanager.error.title": "§b§4Error:§r", 39 | "modmanager.error.container.notFound": "El mod que estás intentando actualizar ha sido desactivado en el transcurso\nEsto no debería ser posible", 40 | "modmanager.error.unknown": "Ha ocurrido un error desconocido al buscar la información:\n %s", 41 | "modmanager.error.unknown.install": "Ha ocurrido un error desconocido durante la instalación:\n %s", 42 | "modmanager.error.unknown.update": "Ha ocurrido un error desconocido durante la actualización:\n %s", 43 | "modmanager.error.invalidStatus": "Se ha recibido un código de estado inválido:\n %d", 44 | "modmanager.error.network": "Error de red al buscar la información:\n %s", 45 | "modmanager.error.failedToParse": "No se ha podido procesar la respuesta:\n %s", 46 | "modmanager.error.noCompatibleModVersionFound": "¡No se ha encontrado una versión compatible para tu canal actual!\nIntenta cambiar de canal en la configuración de Mod Manager", 47 | "modmanager.error.noProviderSelected": "¡El proveedor seleccionado %s no está disponible!", 48 | "modmanager.error.update.noFabricJar": "¡El autor del mod ha marcado a una versión compatible\ncon Fabric pero no ha proveído un archivo jar!", 49 | "modmanager.error.jar.notFound": "El archivo jar no se ha encontrado. ¿Es parte de otro mod?", 50 | "modmanager.error.jar.failedDelete": "No se ha podido eliminar el mod:\n %s", 51 | "modmanager.error.invalidHash": "El archivo descargado probablemente esté roto ya que el hash no coincide\nIntenta checando tu red o contacta al autor del mod", 52 | "modmanager.status.installing": "Instalando %s...", 53 | "modmanager.status.install.success": "¡%s se ha instalado con éxito!\n¿Te gustaría regresar?", 54 | "modmanager.status.updating": "Actualizando %s...", 55 | "modmanager.status.update.success": "¡%s se ha actualizado con éxito!\n¿Te gustaría regresar?", 56 | "modmanager.sorting.sort": "Ordenar por", 57 | "modmanager.sorting.relevance": "Relevancia", 58 | "modmanager.sorting.downloads": "Descargas", 59 | "modmanager.sorting.updated": "Actualizados", 60 | "modmanager.sorting.newest": "Nuevos", 61 | "modmanager.search": "Buscar...", 62 | "modmanager.page.next": "Siguiente página", 63 | "modmanager.page.previous": "Página anterior", 64 | "modmanager.provider.info": "Permite cambiar el proveedor predeterminado para buscar", 65 | "modmanager.toast.update.title": "¡Actualizaciones disponibles!", 66 | "modmanager.toast.update.description": "%d mods tienen actualizaciones", 67 | "modmanager.title.updating": "Actualizando todos los mods..." 68 | } -------------------------------------------------------------------------------- /src/main/resources/assets/modmanager/lang/es_cl.json: -------------------------------------------------------------------------------- 1 | { 2 | "modmanager.badge.installed": "Instalado", 3 | "modmanager.badge.outdated": "Obsoleto", 4 | "modmanager.button.update": "Actualizar", 5 | "modmanager.button.install": "Instalar", 6 | "modmanager.button.remove": "Desinstalar", 7 | "modmanager.button.updateAll": "Actualizar todo", 8 | "modmanager.button.tryAgain": "Reintentar", 9 | "modmanager.button.defaultProvider": "Proveedor predeterminado", 10 | "modmanager.button.updateChannel": "Canal de actualizaciones", 11 | "modmanager.button.save": "Guardar", 12 | "modmanager.button.hide": "Ocultar actualizaciones", 13 | "modmanager.button.show": "Mostrar actualizaciones", 14 | "modmanager.button.open": "Abrir ModManager", 15 | "modmanager.categories": "Categorías", 16 | "modmanager.category.updatable": "Mods con actualizaciones", 17 | "modmanager.category.technology": "Tecnología", 18 | "modmanager.category.adventure": "Aventura", 19 | "modmanager.category.magic": "Magia", 20 | "modmanager.category.utility": "Utilidad", 21 | "modmanager.category.decoration": "Decoración", 22 | "modmanager.category.library": "Librería", 23 | "modmanager.category.cursed": "Cursed", 24 | "modmanager.category.worldgen": "Gen. de Mundo", 25 | "modmanager.category.storage": "Almacenaje", 26 | "modmanager.category.search": "Buscar", 27 | "modmanager.category.food": "Comida", 28 | "modmanager.category.equipment": "Equipamento", 29 | "modmanager.category.misc": "Misceláneo", 30 | "modmanager.changes.title": "§e§lSe han detectado cambios§r", 31 | "modmanager.changes.message": "Los cambios surtirán efecto tras reiniciar.\n¿Desear salir de Minecraft ahora?", 32 | "modmanager.channel.info": "Cambia entre las versiones beta y estables", 33 | "modmanager.channel.all": "Todo", 34 | "modmanager.channel.stable": "Estable", 35 | "modmanager.channel.unstable": "Inestable", 36 | "modmanager.details.author": "Por %s", 37 | "modmanager.details.versioning": "De %s a %s", 38 | "modmanager.error.title": "§b§4Error:§r", 39 | "modmanager.error.container.notFound": "El mod que estás intentando actualizar ha sido desactivado en el transcurso\nEsto no debería ser posible", 40 | "modmanager.error.unknown": "Ha ocurrido un error desconocido al buscar la información:\n %s", 41 | "modmanager.error.unknown.install": "Ha ocurrido un error desconocido durante la instalación:\n %s", 42 | "modmanager.error.unknown.update": "Ha ocurrido un error desconocido durante la actualización:\n %s", 43 | "modmanager.error.invalidStatus": "Se ha recibido un código de estado inválido:\n %d", 44 | "modmanager.error.network": "Error de red al buscar la información:\n %s", 45 | "modmanager.error.failedToParse": "No se ha podido procesar la respuesta:\n %s", 46 | "modmanager.error.noCompatibleModVersionFound": "¡No se ha encontrado una versión compatible para tu canal actual!\nIntenta cambiar de canal en la configuración de Mod Manager", 47 | "modmanager.error.noProviderSelected": "¡El proveedor seleccionado %s no está disponible!", 48 | "modmanager.error.update.noFabricJar": "¡El autor del mod ha marcado a una versión compatible\ncon Fabric pero no ha proveído un archivo jar!", 49 | "modmanager.error.jar.notFound": "El archivo jar no se ha encontrado. ¿Es parte de otro mod?", 50 | "modmanager.error.jar.failedDelete": "No se ha podido eliminar el mod:\n %s", 51 | "modmanager.error.invalidHash": "El archivo descargado probablemente esté roto ya que el hash no coincide\nIntenta checando tu red o contacta al autor del mod", 52 | "modmanager.status.installing": "Instalando %s...", 53 | "modmanager.status.install.success": "¡%s se ha instalado con éxito!\n¿Te gustaría regresar?", 54 | "modmanager.status.updating": "Actualizando %s...", 55 | "modmanager.status.update.success": "¡%s se ha actualizado con éxito!\n¿Te gustaría regresar?", 56 | "modmanager.sorting.sort": "Ordenar por", 57 | "modmanager.sorting.relevance": "Relevancia", 58 | "modmanager.sorting.downloads": "Descargas", 59 | "modmanager.sorting.updated": "Actualizados", 60 | "modmanager.sorting.newest": "Nuevos", 61 | "modmanager.search": "Buscar...", 62 | "modmanager.page.next": "Siguiente página", 63 | "modmanager.page.previous": "Página anterior", 64 | "modmanager.provider.info": "Permite cambiar el proveedor predeterminado para buscar", 65 | "modmanager.toast.update.title": "¡Actualizaciones disponibles!", 66 | "modmanager.toast.update.description": "%d mods tienen actualizaciones", 67 | "modmanager.title.updating": "Actualizando todos los mods..." 68 | } -------------------------------------------------------------------------------- /src/main/resources/assets/modmanager/lang/es_ec.json: -------------------------------------------------------------------------------- 1 | { 2 | "modmanager.badge.installed": "Instalado", 3 | "modmanager.badge.outdated": "Obsoleto", 4 | "modmanager.button.update": "Actualizar", 5 | "modmanager.button.install": "Instalar", 6 | "modmanager.button.remove": "Desinstalar", 7 | "modmanager.button.updateAll": "Actualizar todo", 8 | "modmanager.button.tryAgain": "Reintentar", 9 | "modmanager.button.defaultProvider": "Proveedor predeterminado", 10 | "modmanager.button.updateChannel": "Canal de actualizaciones", 11 | "modmanager.button.save": "Guardar", 12 | "modmanager.button.hide": "Ocultar actualizaciones", 13 | "modmanager.button.show": "Mostrar actualizaciones", 14 | "modmanager.button.open": "Abrir ModManager", 15 | "modmanager.categories": "Categorías", 16 | "modmanager.category.updatable": "Mods con actualizaciones", 17 | "modmanager.category.technology": "Tecnología", 18 | "modmanager.category.adventure": "Aventura", 19 | "modmanager.category.magic": "Magia", 20 | "modmanager.category.utility": "Utilidad", 21 | "modmanager.category.decoration": "Decoración", 22 | "modmanager.category.library": "Librería", 23 | "modmanager.category.cursed": "Cursed", 24 | "modmanager.category.worldgen": "Gen. de Mundo", 25 | "modmanager.category.storage": "Almacenaje", 26 | "modmanager.category.search": "Buscar", 27 | "modmanager.category.food": "Comida", 28 | "modmanager.category.equipment": "Equipamento", 29 | "modmanager.category.misc": "Misceláneo", 30 | "modmanager.changes.title": "§e§lSe han detectado cambios§r", 31 | "modmanager.changes.message": "Los cambios surtirán efecto tras reiniciar.\n¿Desear salir de Minecraft ahora?", 32 | "modmanager.channel.info": "Cambia entre las versiones beta y estables", 33 | "modmanager.channel.all": "Todo", 34 | "modmanager.channel.stable": "Estable", 35 | "modmanager.channel.unstable": "Inestable", 36 | "modmanager.details.author": "Por %s", 37 | "modmanager.details.versioning": "De %s a %s", 38 | "modmanager.error.title": "§b§4Error:§r", 39 | "modmanager.error.container.notFound": "El mod que estás intentando actualizar ha sido desactivado en el transcurso\nEsto no debería ser posible", 40 | "modmanager.error.unknown": "Ha ocurrido un error desconocido al buscar la información:\n %s", 41 | "modmanager.error.unknown.install": "Ha ocurrido un error desconocido durante la instalación:\n %s", 42 | "modmanager.error.unknown.update": "Ha ocurrido un error desconocido durante la actualización:\n %s", 43 | "modmanager.error.invalidStatus": "Se ha recibido un código de estado inválido:\n %d", 44 | "modmanager.error.network": "Error de red al buscar la información:\n %s", 45 | "modmanager.error.failedToParse": "No se ha podido procesar la respuesta:\n %s", 46 | "modmanager.error.noCompatibleModVersionFound": "¡No se ha encontrado una versión compatible para tu canal actual!\nIntenta cambiar de canal en la configuración de Mod Manager", 47 | "modmanager.error.noProviderSelected": "¡El proveedor seleccionado %s no está disponible!", 48 | "modmanager.error.update.noFabricJar": "¡El autor del mod ha marcado a una versión compatible\ncon Fabric pero no ha proveído un archivo jar!", 49 | "modmanager.error.jar.notFound": "El archivo jar no se ha encontrado. ¿Es parte de otro mod?", 50 | "modmanager.error.jar.failedDelete": "No se ha podido eliminar el mod:\n %s", 51 | "modmanager.error.invalidHash": "El archivo descargado probablemente esté roto ya que el hash no coincide\nIntenta checando tu red o contacta al autor del mod", 52 | "modmanager.status.installing": "Instalando %s...", 53 | "modmanager.status.install.success": "¡%s se ha instalado con éxito!\n¿Te gustaría regresar?", 54 | "modmanager.status.updating": "Actualizando %s...", 55 | "modmanager.status.update.success": "¡%s se ha actualizado con éxito!\n¿Te gustaría regresar?", 56 | "modmanager.sorting.sort": "Ordenar por", 57 | "modmanager.sorting.relevance": "Relevancia", 58 | "modmanager.sorting.downloads": "Descargas", 59 | "modmanager.sorting.updated": "Actualizados", 60 | "modmanager.sorting.newest": "Nuevos", 61 | "modmanager.search": "Buscar...", 62 | "modmanager.page.next": "Siguiente página", 63 | "modmanager.page.previous": "Página anterior", 64 | "modmanager.provider.info": "Permite cambiar el proveedor predeterminado para buscar", 65 | "modmanager.toast.update.title": "¡Actualizaciones disponibles!", 66 | "modmanager.toast.update.description": "%d mods tienen actualizaciones", 67 | "modmanager.title.updating": "Actualizando todos los mods..." 68 | } -------------------------------------------------------------------------------- /src/main/resources/assets/modmanager/lang/es_es.json: -------------------------------------------------------------------------------- 1 | { 2 | "modmanager.badge.installed": "Instalado", 3 | "modmanager.badge.outdated": "Obsoleto", 4 | "modmanager.button.update": "Actualizar", 5 | "modmanager.button.install": "Instalar", 6 | "modmanager.button.remove": "Desinstalar", 7 | "modmanager.button.updateAll": "Actualizar todo", 8 | "modmanager.button.tryAgain": "Reintentar", 9 | "modmanager.button.defaultProvider": "Proveedor predeterminado", 10 | "modmanager.button.updateChannel": "Canal de actualizaciones", 11 | "modmanager.button.save": "Guardar", 12 | "modmanager.button.hide": "Ocultar actualizaciones", 13 | "modmanager.button.show": "Mostrar actualizaciones", 14 | "modmanager.button.open": "Abrir ModManager", 15 | "modmanager.categories": "Categorías", 16 | "modmanager.category.updatable": "Mods con actualizaciones", 17 | "modmanager.category.technology": "Tecnología", 18 | "modmanager.category.adventure": "Aventura", 19 | "modmanager.category.magic": "Magia", 20 | "modmanager.category.utility": "Utilidad", 21 | "modmanager.category.decoration": "Decoración", 22 | "modmanager.category.library": "Librería", 23 | "modmanager.category.cursed": "Cursed", 24 | "modmanager.category.worldgen": "Gen. de Mundo", 25 | "modmanager.category.storage": "Almacenaje", 26 | "modmanager.category.search": "Buscar", 27 | "modmanager.category.food": "Comida", 28 | "modmanager.category.equipment": "Equipamento", 29 | "modmanager.category.misc": "Misceláneo", 30 | "modmanager.changes.title": "§e§lSe han detectado cambios§r", 31 | "modmanager.changes.message": "Los cambios surtirán efecto tras reiniciar.\n¿Desear salir de Minecraft ahora?", 32 | "modmanager.channel.info": "Cambia entre las versiones beta y estables", 33 | "modmanager.channel.all": "Todo", 34 | "modmanager.channel.stable": "Estable", 35 | "modmanager.channel.unstable": "Inestable", 36 | "modmanager.details.author": "Por %s", 37 | "modmanager.details.versioning": "De %s a %s", 38 | "modmanager.error.title": "§b§4Error:§r", 39 | "modmanager.error.container.notFound": "El mod que estás intentando actualizar ha sido desactivado en el transcurso\nEsto no debería ser posible", 40 | "modmanager.error.unknown": "Ha ocurrido un error desconocido al buscar la información:\n %s", 41 | "modmanager.error.unknown.install": "Ha ocurrido un error desconocido durante la instalación:\n %s", 42 | "modmanager.error.unknown.update": "Ha ocurrido un error desconocido durante la actualización:\n %s", 43 | "modmanager.error.invalidStatus": "Se ha recibido un código de estado inválido:\n %d", 44 | "modmanager.error.network": "Error de red al buscar la información:\n %s", 45 | "modmanager.error.failedToParse": "No se ha podido procesar la respuesta:\n %s", 46 | "modmanager.error.noCompatibleModVersionFound": "¡No se ha encontrado una versión compatible para tu canal actual!\nIntenta cambiar de canal en la configuración de Mod Manager", 47 | "modmanager.error.noProviderSelected": "¡El proveedor seleccionado %s no está disponible!", 48 | "modmanager.error.update.noFabricJar": "¡El autor del mod ha marcado a una versión compatible\ncon Fabric pero no ha proveído un archivo jar!", 49 | "modmanager.error.jar.notFound": "El archivo jar no se ha encontrado. ¿Es parte de otro mod?", 50 | "modmanager.error.jar.failedDelete": "No se ha podido eliminar el mod:\n %s", 51 | "modmanager.error.invalidHash": "El archivo descargado probablemente esté roto ya que el hash no coincide\nIntenta checando tu red o contacta al autor del mod", 52 | "modmanager.status.installing": "Instalando %s...", 53 | "modmanager.status.install.success": "¡%s se ha instalado con éxito!\n¿Te gustaría regresar?", 54 | "modmanager.status.updating": "Actualizando %s...", 55 | "modmanager.status.update.success": "¡%s se ha actualizado con éxito!\n¿Te gustaría regresar?", 56 | "modmanager.sorting.sort": "Ordenar por", 57 | "modmanager.sorting.relevance": "Relevancia", 58 | "modmanager.sorting.downloads": "Descargas", 59 | "modmanager.sorting.updated": "Actualizados", 60 | "modmanager.sorting.newest": "Nuevos", 61 | "modmanager.search": "Buscar...", 62 | "modmanager.page.next": "Siguiente página", 63 | "modmanager.page.previous": "Página anterior", 64 | "modmanager.provider.info": "Permite cambiar el proveedor predeterminado para buscar", 65 | "modmanager.toast.update.title": "¡Actualizaciones disponibles!", 66 | "modmanager.toast.update.description": "%d mods tienen actualizaciones", 67 | "modmanager.title.updating": "Actualizando todos los mods..." 68 | } -------------------------------------------------------------------------------- /src/main/resources/assets/modmanager/lang/es_mx.json: -------------------------------------------------------------------------------- 1 | { 2 | "modmanager.badge.installed": "Instalado", 3 | "modmanager.badge.outdated": "Obsoleto", 4 | "modmanager.button.update": "Actualizar", 5 | "modmanager.button.install": "Instalar", 6 | "modmanager.button.remove": "Desinstalar", 7 | "modmanager.button.updateAll": "Actualizar todo", 8 | "modmanager.button.tryAgain": "Reintentar", 9 | "modmanager.button.defaultProvider": "Proveedor predeterminado", 10 | "modmanager.button.updateChannel": "Canal de actualizaciones", 11 | "modmanager.button.save": "Guardar", 12 | "modmanager.button.hide": "Ocultar actualizaciones", 13 | "modmanager.button.show": "Mostrar actualizaciones", 14 | "modmanager.button.open": "Abrir ModManager", 15 | "modmanager.categories": "Categorías", 16 | "modmanager.category.updatable": "Mods con actualizaciones", 17 | "modmanager.category.technology": "Tecnología", 18 | "modmanager.category.adventure": "Aventura", 19 | "modmanager.category.magic": "Magia", 20 | "modmanager.category.utility": "Utilidad", 21 | "modmanager.category.decoration": "Decoración", 22 | "modmanager.category.library": "Librería", 23 | "modmanager.category.cursed": "Cursed", 24 | "modmanager.category.worldgen": "Gen. de Mundo", 25 | "modmanager.category.storage": "Almacenaje", 26 | "modmanager.category.search": "Buscar", 27 | "modmanager.category.food": "Comida", 28 | "modmanager.category.equipment": "Equipamento", 29 | "modmanager.category.misc": "Misceláneo", 30 | "modmanager.changes.title": "§e§lSe han detectado cambios§r", 31 | "modmanager.changes.message": "Los cambios surtirán efecto tras reiniciar.\n¿Desear salir de Minecraft ahora?", 32 | "modmanager.channel.info": "Cambia entre las versiones beta y estables", 33 | "modmanager.channel.all": "Todo", 34 | "modmanager.channel.stable": "Estable", 35 | "modmanager.channel.unstable": "Inestable", 36 | "modmanager.details.author": "Por %s", 37 | "modmanager.details.versioning": "De %s a %s", 38 | "modmanager.error.title": "§b§4Error:§r", 39 | "modmanager.error.container.notFound": "El mod que estás intentando actualizar ha sido desactivado en el transcurso\nEsto no debería ser posible", 40 | "modmanager.error.unknown": "Ha ocurrido un error desconocido al buscar la información:\n %s", 41 | "modmanager.error.unknown.install": "Ha ocurrido un error desconocido durante la instalación:\n %s", 42 | "modmanager.error.unknown.update": "Ha ocurrido un error desconocido durante la actualización:\n %s", 43 | "modmanager.error.invalidStatus": "Se ha recibido un código de estado inválido:\n %d", 44 | "modmanager.error.network": "Error de red al buscar la información:\n %s", 45 | "modmanager.error.failedToParse": "No se ha podido procesar la respuesta:\n %s", 46 | "modmanager.error.noCompatibleModVersionFound": "¡No se ha encontrado una versión compatible para tu canal actual!\nIntenta cambiar de canal en la configuración de Mod Manager", 47 | "modmanager.error.noProviderSelected": "¡El proveedor seleccionado %s no está disponible!", 48 | "modmanager.error.update.noFabricJar": "¡El autor del mod ha marcado a una versión compatible\ncon Fabric pero no ha proveído un archivo jar!", 49 | "modmanager.error.jar.notFound": "El archivo jar no se ha encontrado. ¿Es parte de otro mod?", 50 | "modmanager.error.jar.failedDelete": "No se ha podido eliminar el mod:\n %s", 51 | "modmanager.error.invalidHash": "El archivo descargado probablemente esté roto ya que el hash no coincide\nIntenta checando tu red o contacta al autor del mod", 52 | "modmanager.status.installing": "Instalando %s...", 53 | "modmanager.status.install.success": "¡%s se ha instalado con éxito!\n¿Te gustaría regresar?", 54 | "modmanager.status.updating": "Actualizando %s...", 55 | "modmanager.status.update.success": "¡%s se ha actualizado con éxito!\n¿Te gustaría regresar?", 56 | "modmanager.sorting.sort": "Ordenar por", 57 | "modmanager.sorting.relevance": "Relevancia", 58 | "modmanager.sorting.downloads": "Descargas", 59 | "modmanager.sorting.updated": "Actualizados", 60 | "modmanager.sorting.newest": "Nuevos", 61 | "modmanager.search": "Buscar...", 62 | "modmanager.page.next": "Siguiente página", 63 | "modmanager.page.previous": "Página anterior", 64 | "modmanager.provider.info": "Permite cambiar el proveedor predeterminado para buscar", 65 | "modmanager.toast.update.title": "¡Actualizaciones disponibles!", 66 | "modmanager.toast.update.description": "%d mods tienen actualizaciones", 67 | "modmanager.title.updating": "Actualizando todos los mods..." 68 | } -------------------------------------------------------------------------------- /src/main/resources/assets/modmanager/lang/es_uy.json: -------------------------------------------------------------------------------- 1 | { 2 | "modmanager.badge.installed": "Instalado", 3 | "modmanager.badge.outdated": "Obsoleto", 4 | "modmanager.button.update": "Actualizar", 5 | "modmanager.button.install": "Instalar", 6 | "modmanager.button.remove": "Desinstalar", 7 | "modmanager.button.updateAll": "Actualizar todo", 8 | "modmanager.button.tryAgain": "Reintentar", 9 | "modmanager.button.defaultProvider": "Proveedor predeterminado", 10 | "modmanager.button.updateChannel": "Canal de actualizaciones", 11 | "modmanager.button.save": "Guardar", 12 | "modmanager.button.hide": "Ocultar actualizaciones", 13 | "modmanager.button.show": "Mostrar actualizaciones", 14 | "modmanager.button.open": "Abrir ModManager", 15 | "modmanager.categories": "Categorías", 16 | "modmanager.category.updatable": "Mods con actualizaciones", 17 | "modmanager.category.technology": "Tecnología", 18 | "modmanager.category.adventure": "Aventura", 19 | "modmanager.category.magic": "Magia", 20 | "modmanager.category.utility": "Utilidad", 21 | "modmanager.category.decoration": "Decoración", 22 | "modmanager.category.library": "Librería", 23 | "modmanager.category.cursed": "Cursed", 24 | "modmanager.category.worldgen": "Gen. de Mundo", 25 | "modmanager.category.storage": "Almacenaje", 26 | "modmanager.category.search": "Buscar", 27 | "modmanager.category.food": "Comida", 28 | "modmanager.category.equipment": "Equipamento", 29 | "modmanager.category.misc": "Misceláneo", 30 | "modmanager.changes.title": "§e§lSe han detectado cambios§r", 31 | "modmanager.changes.message": "Los cambios surtirán efecto tras reiniciar.\n¿Desear salir de Minecraft ahora?", 32 | "modmanager.channel.info": "Cambia entre las versiones beta y estables", 33 | "modmanager.channel.all": "Todo", 34 | "modmanager.channel.stable": "Estable", 35 | "modmanager.channel.unstable": "Inestable", 36 | "modmanager.details.author": "Por %s", 37 | "modmanager.details.versioning": "De %s a %s", 38 | "modmanager.error.title": "§b§4Error:§r", 39 | "modmanager.error.container.notFound": "El mod que estás intentando actualizar ha sido desactivado en el transcurso\nEsto no debería ser posible", 40 | "modmanager.error.unknown": "Ha ocurrido un error desconocido al buscar la información:\n %s", 41 | "modmanager.error.unknown.install": "Ha ocurrido un error desconocido durante la instalación:\n %s", 42 | "modmanager.error.unknown.update": "Ha ocurrido un error desconocido durante la actualización:\n %s", 43 | "modmanager.error.invalidStatus": "Se ha recibido un código de estado inválido:\n %d", 44 | "modmanager.error.network": "Error de red al buscar la información:\n %s", 45 | "modmanager.error.failedToParse": "No se ha podido procesar la respuesta:\n %s", 46 | "modmanager.error.noCompatibleModVersionFound": "¡No se ha encontrado una versión compatible para tu canal actual!\nIntenta cambiar de canal en la configuración de Mod Manager", 47 | "modmanager.error.noProviderSelected": "¡El proveedor seleccionado %s no está disponible!", 48 | "modmanager.error.update.noFabricJar": "¡El autor del mod ha marcado a una versión compatible\ncon Fabric pero no ha proveído un archivo jar!", 49 | "modmanager.error.jar.notFound": "El archivo jar no se ha encontrado. ¿Es parte de otro mod?", 50 | "modmanager.error.jar.failedDelete": "No se ha podido eliminar el mod:\n %s", 51 | "modmanager.error.invalidHash": "El archivo descargado probablemente esté roto ya que el hash no coincide\nIntenta checando tu red o contacta al autor del mod", 52 | "modmanager.status.installing": "Instalando %s...", 53 | "modmanager.status.install.success": "¡%s se ha instalado con éxito!\n¿Te gustaría regresar?", 54 | "modmanager.status.updating": "Actualizando %s...", 55 | "modmanager.status.update.success": "¡%s se ha actualizado con éxito!\n¿Te gustaría regresar?", 56 | "modmanager.sorting.sort": "Ordenar por", 57 | "modmanager.sorting.relevance": "Relevancia", 58 | "modmanager.sorting.downloads": "Descargas", 59 | "modmanager.sorting.updated": "Actualizados", 60 | "modmanager.sorting.newest": "Nuevos", 61 | "modmanager.search": "Buscar...", 62 | "modmanager.page.next": "Siguiente página", 63 | "modmanager.page.previous": "Página anterior", 64 | "modmanager.provider.info": "Permite cambiar el proveedor predeterminado para buscar", 65 | "modmanager.toast.update.title": "¡Actualizaciones disponibles!", 66 | "modmanager.toast.update.description": "%d mods tienen actualizaciones", 67 | "modmanager.title.updating": "Actualizando todos los mods..." 68 | } -------------------------------------------------------------------------------- /src/main/resources/assets/modmanager/lang/es_ve.json: -------------------------------------------------------------------------------- 1 | { 2 | "modmanager.badge.installed": "Instalado", 3 | "modmanager.badge.outdated": "Obsoleto", 4 | "modmanager.button.update": "Actualizar", 5 | "modmanager.button.install": "Instalar", 6 | "modmanager.button.remove": "Desinstalar", 7 | "modmanager.button.updateAll": "Actualizar todo", 8 | "modmanager.button.tryAgain": "Reintentar", 9 | "modmanager.button.defaultProvider": "Proveedor predeterminado", 10 | "modmanager.button.updateChannel": "Canal de actualizaciones", 11 | "modmanager.button.save": "Guardar", 12 | "modmanager.button.hide": "Ocultar actualizaciones", 13 | "modmanager.button.show": "Mostrar actualizaciones", 14 | "modmanager.button.open": "Abrir ModManager", 15 | "modmanager.categories": "Categorías", 16 | "modmanager.category.updatable": "Mods con actualizaciones", 17 | "modmanager.category.technology": "Tecnología", 18 | "modmanager.category.adventure": "Aventura", 19 | "modmanager.category.magic": "Magia", 20 | "modmanager.category.utility": "Utilidad", 21 | "modmanager.category.decoration": "Decoración", 22 | "modmanager.category.library": "Librería", 23 | "modmanager.category.cursed": "Cursed", 24 | "modmanager.category.worldgen": "Gen. de Mundo", 25 | "modmanager.category.storage": "Almacenaje", 26 | "modmanager.category.search": "Buscar", 27 | "modmanager.category.food": "Comida", 28 | "modmanager.category.equipment": "Equipamento", 29 | "modmanager.category.misc": "Misceláneo", 30 | "modmanager.changes.title": "§e§lSe han detectado cambios§r", 31 | "modmanager.changes.message": "Los cambios surtirán efecto tras reiniciar.\n¿Desear salir de Minecraft ahora?", 32 | "modmanager.channel.info": "Cambia entre las versiones beta y estables", 33 | "modmanager.channel.all": "Todo", 34 | "modmanager.channel.stable": "Estable", 35 | "modmanager.channel.unstable": "Inestable", 36 | "modmanager.details.author": "Por %s", 37 | "modmanager.details.versioning": "De %s a %s", 38 | "modmanager.error.title": "§b§4Error:§r", 39 | "modmanager.error.container.notFound": "El mod que estás intentando actualizar ha sido desactivado en el transcurso\nEsto no debería ser posible", 40 | "modmanager.error.unknown": "Ha ocurrido un error desconocido al buscar la información:\n %s", 41 | "modmanager.error.unknown.install": "Ha ocurrido un error desconocido durante la instalación:\n %s", 42 | "modmanager.error.unknown.update": "Ha ocurrido un error desconocido durante la actualización:\n %s", 43 | "modmanager.error.invalidStatus": "Se ha recibido un código de estado inválido:\n %d", 44 | "modmanager.error.network": "Error de red al buscar la información:\n %s", 45 | "modmanager.error.failedToParse": "No se ha podido procesar la respuesta:\n %s", 46 | "modmanager.error.noCompatibleModVersionFound": "¡No se ha encontrado una versión compatible para tu canal actual!\nIntenta cambiar de canal en la configuración de Mod Manager", 47 | "modmanager.error.noProviderSelected": "¡El proveedor seleccionado %s no está disponible!", 48 | "modmanager.error.update.noFabricJar": "¡El autor del mod ha marcado a una versión compatible\ncon Fabric pero no ha proveído un archivo jar!", 49 | "modmanager.error.jar.notFound": "El archivo jar no se ha encontrado. ¿Es parte de otro mod?", 50 | "modmanager.error.jar.failedDelete": "No se ha podido eliminar el mod:\n %s", 51 | "modmanager.error.invalidHash": "El archivo descargado probablemente esté roto ya que el hash no coincide\nIntenta checando tu red o contacta al autor del mod", 52 | "modmanager.status.installing": "Instalando %s...", 53 | "modmanager.status.install.success": "¡%s se ha instalado con éxito!\n¿Te gustaría regresar?", 54 | "modmanager.status.updating": "Actualizando %s...", 55 | "modmanager.status.update.success": "¡%s se ha actualizado con éxito!\n¿Te gustaría regresar?", 56 | "modmanager.sorting.sort": "Ordenar por", 57 | "modmanager.sorting.relevance": "Relevancia", 58 | "modmanager.sorting.downloads": "Descargas", 59 | "modmanager.sorting.updated": "Actualizados", 60 | "modmanager.sorting.newest": "Nuevos", 61 | "modmanager.search": "Buscar...", 62 | "modmanager.page.next": "Siguiente página", 63 | "modmanager.page.previous": "Página anterior", 64 | "modmanager.provider.info": "Permite cambiar el proveedor predeterminado para buscar", 65 | "modmanager.toast.update.title": "¡Actualizaciones disponibles!", 66 | "modmanager.toast.update.description": "%d mods tienen actualizaciones", 67 | "modmanager.title.updating": "Actualizando todos los mods..." 68 | } -------------------------------------------------------------------------------- /src/main/resources/assets/modmanager/lang/ko_kr.json: -------------------------------------------------------------------------------- 1 | { 2 | "modmanager.badge.installed": "설치됨", 3 | "modmanager.badge.outdated": "이전 버전", 4 | "modmanager.button.update": "업데이트", 5 | "modmanager.button.install": "설치", 6 | "modmanager.button.remove": "제거", 7 | "modmanager.button.updateAll": "모두 업데이트", 8 | "modmanager.button.tryAgain": "다시 시도", 9 | "modmanager.button.defaultProvider": "기본 공급자", 10 | "modmanager.button.updateChannel": "업데이트 채널", 11 | "modmanager.button.save": "저장", 12 | "modmanager.button.hide": "업데이트 숨기기", 13 | "modmanager.button.show": "업데이트 보이기", 14 | "modmanager.button.open": "ModManager 열기", 15 | "modmanager.categories": "카테고리", 16 | "modmanager.category.updatable": "업데이트 가능한 모드", 17 | "modmanager.category.technology": "기술", 18 | "modmanager.category.adventure": "모험", 19 | "modmanager.category.magic": "마법", 20 | "modmanager.category.utility": "도구", 21 | "modmanager.category.decoration": "장식", 22 | "modmanager.category.library": "라이브러리", 23 | "modmanager.category.cursed": "저주받은 모드", 24 | "modmanager.category.worldgen": "월드 생성", 25 | "modmanager.category.storage": "저장고", 26 | "modmanager.category.search": "검색", 27 | "modmanager.category.food": "음식", 28 | "modmanager.category.equipment": "장비", 29 | "modmanager.category.misc": "기타", 30 | "modmanager.changes.title": "§e§l변경사항 감지됨§r", 31 | "modmanager.changes.message": "변경사항은 클라이언트를 재시작하면 적용됩니다.\nMinecraft를 지금 재시작 하시겠습니까?", 32 | "modmanager.channel.info": "베타와 안정 버전 교체", 33 | "modmanager.channel.all": "전체", 34 | "modmanager.channel.stable": "안정", 35 | "modmanager.channel.unstable": "불안정", 36 | "modmanager.details.author": "게시자: %s", 37 | "modmanager.details.versioning": "%s 에서 %s (으)로", 38 | "modmanager.error.title": "§b§4오류:§r", 39 | "modmanager.error.container.notFound": "모드를 업데이트하는 도중 언로드되었습니다\n업데이트가 불가능합니다", 40 | "modmanager.error.unknown": "데이터를 검색하는 도중 알 수 없는 오류가 발생했습니다:\n %s", 41 | "modmanager.error.unknown.install": "설치를 진행하는 도중 알 수 없는 오류가 발생했습니다:\n %s", 42 | "modmanager.error.unknown.update": "업데이트를 진행하는 도중 알 수 없는 오류가 발생했습니다:\n %s", 43 | "modmanager.error.invalidStatus": "잘못된 상태 코드를 받았습니다:\n %d", 44 | "modmanager.error.network": "데이터를 검색하는 도중 네트워크 오류가 발생했습니다:\n %s", 45 | "modmanager.error.failedToParse": "응답을 분석하지 못했습니다:\n %s", 46 | "modmanager.error.noCompatibleModVersionFound": "현재 채널에 맞는 버전을 찾을 수 없습니다!\nMod Manager 설정에서 채널을 변경하십시오!", 47 | "modmanager.error.noProviderSelected": "선택한 공급자 %s 는 지금 사용할 수 없습니다!", 48 | "modmanager.error.update.noFabricJar": "모드 작성자가 패브릭과 호환되는 버전으로 표시했지만\n파일을 제공하지 않았습니다!", 49 | "modmanager.error.jar.notFound": "오류, jar 파일을 찾을 수 없습니다. 모드의 일부인가요?", 50 | "modmanager.error.jar.failedDelete": "모드를 제거하는데 실패했습니다:\n %s", 51 | "modmanager.error.invalidHash": "오류, %s 해쉬값이 맞지 않습니다. 모드가 고장났을 수 있습니다\n당신의 네트워크를 확인하거나 모드 작성자에게 문의하십시오", 52 | "modmanager.status.installing": "%s 설치중...", 53 | "modmanager.status.install.success": "성공적으로 %s 을(를) 설치했습니다!\n돌아가시겠습니까?", 54 | "modmanager.status.updating": "%s 업데이트중...", 55 | "modmanager.status.update.success": "성공적으로 %s 을(를) 업데이트 했습니다!\n돌아가시겠습니까?", 56 | "modmanager.sorting.sort": "정렬", 57 | "modmanager.sorting.relevance": "관련성", 58 | "modmanager.sorting.downloads": "다운로드 수", 59 | "modmanager.sorting.updated": "업데이트순", 60 | "modmanager.sorting.newest": "공개순", 61 | "modmanager.search": "검색...", 62 | "modmanager.page.next": "다음 페이지", 63 | "modmanager.page.previous": "이전 페이지", 64 | "modmanager.provider.info": "기본 검색 공급자를 변경할 수 있습니다", 65 | "modmanager.toast.update.title": "업데이트가 가능합니다!", 66 | "modmanager.toast.update.description": "%d 모드를 업데이트 할 수 있습니다", 67 | "modmanager.title.updating": "모든 모드를 업데이트 ..." 68 | } 69 | -------------------------------------------------------------------------------- /src/main/resources/assets/modmanager/lang/ru_ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "modmanager.badge.installed": "Установлен", 3 | "modmanager.badge.outdated": "Устарел", 4 | "modmanager.button.update": "Обновить", 5 | "modmanager.button.install": "Установить", 6 | "modmanager.button.remove": "Удалить", 7 | "modmanager.button.updateAll": "Обновить все", 8 | "modmanager.button.tryAgain": "Ещё раз", 9 | "modmanager.button.defaultProvider": "Источник по умолчанию", 10 | "modmanager.button.updateChannel": "Канал обновления", 11 | "modmanager.button.save": "Сохранить", 12 | "modmanager.button.hide": "Скрыть обновления", 13 | "modmanager.button.show": "Показать обновления", 14 | "modmanager.button.open": "Открыть менеджер модов", 15 | "modmanager.categories": "Категории", 16 | "modmanager.category.updatable": "Обновления", 17 | "modmanager.category.technology": "Технологии", 18 | "modmanager.category.adventure": "Приключения", 19 | "modmanager.category.magic": "Магия", 20 | "modmanager.category.utility": "Утилиты", 21 | "modmanager.category.decoration": "Косметика", 22 | "modmanager.category.library": "Библиотеки", 23 | "modmanager.category.cursed": "Проклятые", 24 | "modmanager.category.worldgen": "Генерация мира", 25 | "modmanager.category.storage": "Инвентарь", 26 | "modmanager.category.search": "Поиск", 27 | "modmanager.category.food": "Еда", 28 | "modmanager.category.equipment": "Экипировка", 29 | "modmanager.category.misc": "Разное", 30 | "modmanager.changes.title": "§e§lОбнаружены изменения§r", 31 | "modmanager.changes.message": "Изменения вступят в силу после перезапуска.\nЗакрыть Minecraft сайчас?", 32 | "modmanager.channel.info": "Переключиться между стабильными и бета-версиями", 33 | "modmanager.channel.all": "Все", 34 | "modmanager.channel.stable": "Стабильные", 35 | "modmanager.channel.unstable": "Нестабильные", 36 | "modmanager.details.author": "Автор: %s", 37 | "modmanager.details.versioning": "С %s до %s", 38 | "modmanager.error.title": "§b§4Ошибка:§r", 39 | "modmanager.error.container.notFound": "Мод, который вы пытаетесь обновить, был удалён.\nОбновление не представляется возможным.", 40 | "modmanager.error.unknown": "Неизвестная ошибка при получении данных:\n %s", 41 | "modmanager.error.unknown.install": "Неизвестная ошибка в процессе установки:\n %s", 42 | "modmanager.error.unknown.update": "Неизвестная ошибка в процессе обновления:\n %s", 43 | "modmanager.error.invalidStatus": "Получен неверный код состояния:\n %d", 44 | "modmanager.error.network": "Ошибка подключения при получении данных:\n %s", 45 | "modmanager.error.failedToParse": "Не удалось обработать запрос:\n %s", 46 | "modmanager.error.noCompatibleModVersionFound": "Совместимая версия для вашего текущего канала не найдена.\nПопробуйте изменить канал в настройках Mod Manager.", 47 | "modmanager.error.noProviderSelected": "Выбранный поставщик %s недоступен.", 48 | "modmanager.error.update.noFabricJar": "Автор мода пометил версию совместимой с Fabric,\nно не предоставил её.", 49 | "modmanager.error.jar.notFound": "Ошибка Файл .jar не найден. Возможно, это часть другого мода?", 50 | "modmanager.error.jar.failedDelete": "Не удалось удалить мод:\n %s", 51 | "modmanager.error.invalidHash": "Ошибка Загруженный файл (вероятно) повреждён, так как хэши %s не совпадают.\nПроверьте ваше соединение или свяжитесь с автором мода.", 52 | "modmanager.status.installing": "Установка %s...", 53 | "modmanager.status.install.success": "%s успешно установлен!\nХотите продолжить?", 54 | "modmanager.status.updating": "Обновление...", 55 | "modmanager.status.update.success": "%s успешно обновлён!\nХотите продолжить?", 56 | "modmanager.sorting.sort": "Сортировка", 57 | "modmanager.sorting.relevance": "по популярности", 58 | "modmanager.sorting.downloads": "по скачиваниям", 59 | "modmanager.sorting.updated": "недавно обновлённые", 60 | "modmanager.sorting.newest": "новейшие", 61 | "modmanager.search": "Поиск...", 62 | "modmanager.page.next": "Следующая страница", 63 | "modmanager.page.previous": "Предыдущая страница", 64 | "modmanager.provider.info": "Позволяет изменить источник по умолчанию для просмотра", 65 | "modmanager.toast.update.title": "Доступны обновления!", 66 | "modmanager.toast.update.description": "Можно обновить модов: %d", 67 | "modmanager.title.updating": "Обновление всех модов..." 68 | } 69 | -------------------------------------------------------------------------------- /src/main/resources/assets/modmanager/lang/tr_tr.json: -------------------------------------------------------------------------------- 1 | { 2 | "modmanager.badge.installed": "Kurulu", 3 | "modmanager.badge.outdated": "Eski", 4 | "modmanager.button.update": "Güncelle", 5 | "modmanager.button.install": "Kur", 6 | "modmanager.button.remove": "Kaldır", 7 | "modmanager.button.updateAll": "Tümünü güncelle", 8 | "modmanager.button.tryAgain": "Tekrar dene", 9 | "modmanager.button.defaultProvider": "Varsayılan sağlayıcı", 10 | "modmanager.button.updateChannel": "Güncelleme kanalı", 11 | "modmanager.button.save": "Kaydet", 12 | "modmanager.categories": "Kategoriler", 13 | "modmanager.category.updatable": "Güncellenebilir Modlar", 14 | "modmanager.category.technology": "Teknoloji", 15 | "modmanager.category.adventure": "Macera", 16 | "modmanager.category.magic": "Büyü", 17 | "modmanager.category.utility": "Kullanışlı", 18 | "modmanager.category.decoration": "Dekorasyon", 19 | "modmanager.category.library": "Kütpahane", 20 | "modmanager.category.cursed": "Lanetli", 21 | "modmanager.category.worldgen": "Dünya Oluşumu", 22 | "modmanager.category.storage": "Depolama", 23 | "modmanager.category.search": "Ara", 24 | "modmanager.category.food": "Yiyecek", 25 | "modmanager.category.equipment": "Ekipman", 26 | "modmanager.category.misc": "Çeşitli", 27 | "modmanager.changes.title": "§e§lDeğişiklik algılandı§r", 28 | "modmanager.changes.message": "Yapılan değişiklikler, oyunu yeniden başlattıktan sonra uygulanacaktır.\nMinecraft'ı şimdi kapat?", 29 | "modmanager.channel.info": "Beta ve tam sürüm arasında geçiş yap", 30 | "modmanager.channel.all": "Tümü", 31 | "modmanager.channel.stable": "Kararlı", 32 | "modmanager.channel.unstable": "Kararsız", 33 | "modmanager.details.author": "%s tarafından", 34 | "modmanager.details.versioning": "%s sürümünden, %s sürümüne", 35 | "modmanager.error.title": "§b§4Hata:§r", 36 | "modmanager.error.unknown": "Veri alınırken bilinmeyen bir hata meydana geldi:\n %s", 37 | "modmanager.error.unknown.install": "Kurulum yapılırken bilinmeyen bir hata meydana geldi:\n %s", 38 | "modmanager.error.unknown.update": "Güncelleme yapılırken bilinmeyen bir hata meydana geldi:\n %s", 39 | "modmanager.error.invalidStatus": "Geçersiz durum kodu alındı:\n %d", 40 | "modmanager.error.network": "Veri alınırken ağ sorunu meydana geldi:\n %s", 41 | "modmanager.error.failedToParse": "Alınan yanıt, parçalanamadı (Failed to parse response):\n %s", 42 | "modmanager.error.noCompatibleModVersionFound": "Şu anki kanalın için uyumlu sürüm bulunamadı!\nMod Manager ayarlarından kanalı değiştirmeyi dene!", 43 | "modmanager.status.installing": "%s kuruluyor...", 44 | "modmanager.status.install.success": "%s başarıyla kuruldu!\nGeri dönmek istiyor musun?", 45 | "modmanager.status.updating": "%s güncelleniyor...", 46 | "modmanager.status.update.success": "%s başarıyla güncellendi!\nGeri dönmek istiyor musun?", 47 | "modmanager.sorting.sort": "Sırala", 48 | "modmanager.sorting.relevance": "Önerilen", 49 | "modmanager.sorting.downloads": "Yükleme Sayısı", 50 | "modmanager.sorting.updated": "Güncellik", 51 | "modmanager.sorting.newest": "En Yeniler", 52 | "modmanager.search": "Ara...", 53 | "modmanager.page.next": "Sıradaki Sayfa", 54 | "modmanager.page.previous": "Önceki Sayfa", 55 | "modmanager.provider.info": "Arama yapmak için varsayılan sağlayıcını değiştirmeni sağlar", 56 | "modmanager.toast.update.title": "Güncellemeler Mevcut!", 57 | "modmanager.toast.update.description": "%d mod güncellenebilir durumda", 58 | "modmanager.error.jar.notFound": "Error jar file not found. Is it part of another mod?", 59 | "modmanager.error.invalidHash": "Error the downloaded file is probably broken as the %s hashes don't match\nTry checking your network or contact the mod author" 60 | } -------------------------------------------------------------------------------- /src/main/resources/assets/modmanager/lang/zh_cn.json: -------------------------------------------------------------------------------- 1 | { 2 | "modmanager.badge.installed": "已安装", 3 | "modmanager.badge.outdated": "可更新", 4 | "modmanager.button.update": "更新", 5 | "modmanager.button.install": "安装", 6 | "modmanager.button.remove": "卸载", 7 | "modmanager.button.updateAll": "更新全部", 8 | "modmanager.button.tryAgain": "重试", 9 | "modmanager.button.defaultProvider": "默认源", 10 | "modmanager.button.updateChannel": "版本通道", 11 | "modmanager.button.save": "保存", 12 | "modmanager.button.hide": "隐藏更新", 13 | "modmanager.button.show": "显示更新", 14 | "modmanager.button.open": "打开 Mod Manager", 15 | "modmanager.categories": "类别", 16 | "modmanager.category.updatable": "可更新", 17 | "modmanager.category.technology": "科技", 18 | "modmanager.category.adventure": "冒险", 19 | "modmanager.category.magic": "魔法", 20 | "modmanager.category.utility": "辅助", 21 | "modmanager.category.decoration": "装饰", 22 | "modmanager.category.library": "前置", 23 | "modmanager.category.cursed": "魔改", 24 | "modmanager.category.worldgen": "世界生成", 25 | "modmanager.category.storage": "储存", 26 | "modmanager.category.search": "搜索", 27 | "modmanager.category.food": "食物", 28 | "modmanager.category.equipment": "装备", 29 | "modmanager.category.misc": "杂项", 30 | "modmanager.changes.title": "§e§l检测到更改§r", 31 | "modmanager.changes.message": "更改将会在游戏重启后生效。\n现在退出游戏吗?", 32 | "modmanager.channel.info": "在稳定版与测试版模组间切换", 33 | "modmanager.channel.all": "所有", 34 | "modmanager.channel.stable": "稳定版", 35 | "modmanager.channel.unstable": "测试版", 36 | "modmanager.details.author": "作者: %s", 37 | "modmanager.details.versioning": "从%s到%s", 38 | "modmanager.error.title": "§b§4错误: §r", 39 | "modmanager.error.container.notFound": "你试图更新的模组已经被卸载了/这应该是不可能的.", 40 | "modmanager.error.unknown": "检索数据时发生了未知错误:\n %s", 41 | "modmanager.error.unknown.install": "安装过程中发生了未知错误:\n %s", 42 | "modmanager.error.unknown.update": "更新过程中发生了未知错误:\n %s", 43 | "modmanager.error.invalidStatus": "接收到了无效的状态码:\n %d", 44 | "modmanager.error.network": "检索数据时发生了网络问题:\n %s", 45 | "modmanager.error.failedToParse": "无法处理响应:\n %s", 46 | "modmanager.error.noCompatibleModVersionFound": "在当前版本通道下没有找到适合当前版本的模组!\n请尝试在 Mod Manager 设置中更换版本通道。", 47 | "modmanager.error.noProviderSelected": "选定的模组来源%s不可用!", 48 | "modmanager.error.update.noFabricJar": "该模组的作者标记了这个模组有兼容 Fabric 的版本,但实际上并没有为 Fabric 提供一个版本!", 49 | "modmanager.error.jar.notFound": "错误,没有找到 Jar 文件。它是另一个模组的一部分吗?", 50 | "modmanager.error.jar.failedDelete": "删除模组失败:\n %s", 51 | "modmanager.error.invalidHash": "错误,下载的文件可能已经损坏,因为 %s 的哈希值不匹配\n尝试检查您的网络或联系模组作者", 52 | "modmanager.status.installing": "正在安装 %s...", 53 | "modmanager.status.install.success": "%s 已成功安装!\n您想要返回吗?", 54 | "modmanager.status.updating": "正在更新 %s...", 55 | "modmanager.status.update.success": "%s 已成功更新!\n您想要返回吗?", 56 | "modmanager.sorting.sort": "排序依据", 57 | "modmanager.sorting.relevance": "相关性", 58 | "modmanager.sorting.downloads": "下载数", 59 | "modmanager.sorting.updated": "最近更新", 60 | "modmanager.sorting.newest": "最新上传", 61 | "modmanager.search": "搜索...", 62 | "modmanager.page.next": "下一页", 63 | "modmanager.page.previous": "上一页", 64 | "modmanager.provider.info": "允许你更改检索时的默认模组来源", 65 | "modmanager.toast.update.title": "检测到更新!", 66 | "modmanager.toast.update.description": "%d个模组可更新", 67 | "modmanager.title.updating": "正在更新所有模组..." 68 | } 69 | -------------------------------------------------------------------------------- /src/main/resources/assets/modmanager/textures/gui/hide_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModManagerMC/ModManager/c6cd9535504837a97fef3b73d43df3a6c95a2ca3/src/main/resources/assets/modmanager/textures/gui/hide_button.png -------------------------------------------------------------------------------- /src/main/resources/assets/modmanager/textures/gui/install_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModManagerMC/ModManager/c6cd9535504837a97fef3b73d43df3a6c95a2ca3/src/main/resources/assets/modmanager/textures/gui/install_button.png -------------------------------------------------------------------------------- /src/main/resources/assets/modmanager/textures/gui/loading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModManagerMC/ModManager/c6cd9535504837a97fef3b73d43df3a6c95a2ca3/src/main/resources/assets/modmanager/textures/gui/loading.png -------------------------------------------------------------------------------- /src/main/resources/assets/modmanager/textures/gui/show_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModManagerMC/ModManager/c6cd9535504837a97fef3b73d43df3a6c95a2ca3/src/main/resources/assets/modmanager/textures/gui/show_button.png -------------------------------------------------------------------------------- /src/main/resources/build.info: -------------------------------------------------------------------------------- 1 | version=${version} 2 | releaseTarget=${release_target} -------------------------------------------------------------------------------- /src/main/resources/fabric.mod.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": 1, 3 | "id": "modmanager", 4 | "version": "${version}", 5 | "name": "Mod Manager", 6 | "description": "Extends ModMenu with an additional tab where you can install mods from Modrinth", 7 | "authors": [ 8 | "DeathsGun" 9 | ], 10 | "contact": { 11 | "homepage": "https://modrinth.com/mod/modmanager", 12 | "sources": "https://github.com/DeathsGun/ModManager", 13 | "issues": "https://github.com/DeathsGun/ModManager/issues" 14 | }, 15 | "icon": "assets/modmanager/icon.png", 16 | "license": "Apache-2.0", 17 | "environment": "client", 18 | "entrypoints": { 19 | "client": [ 20 | { 21 | "adapter": "kotlin", 22 | "value": "xyz.deathsgun.modmanager.ModManager" 23 | } 24 | ], 25 | "preLaunch": [ 26 | { 27 | "adapter": "kotlin", 28 | "value": "xyz.deathsgun.modmanager.PreLaunchHook" 29 | } 30 | ], 31 | "modmenu": [ 32 | "xyz.deathsgun.modmanager.ModMenuEntrypoint" 33 | ] 34 | }, 35 | "custom": { 36 | "modmanager": { 37 | "modrinth": "6kq7BzRK" 38 | } 39 | }, 40 | "mixins": [ 41 | "modmanager.mixins.json" 42 | ], 43 | "depends": { 44 | "fabricloader": ">=0.12", 45 | "modmenu": "^${modmenu_version}", 46 | "fabric-language-kotlin": ">=${fabric_kotlin_version}", 47 | "minecraft": "1.18.x", 48 | "java": ">=16" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/resources/modmanager.mixins.json: -------------------------------------------------------------------------------- 1 | { 2 | "required": true, 3 | "minVersion": "0.8", 4 | "package": "xyz.deathsgun.modmanager.mixin", 5 | "compatibilityLevel": "JAVA_17", 6 | "mixins": [ 7 | ], 8 | "client": [ 9 | "ModsScreenMixin", 10 | ], 11 | "injectors": { 12 | "defaultRequire": 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/test/kotlin/xyz/deathsgun/modmanager/dummy/DummyModrinthProvider.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 DeathsGun 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package xyz.deathsgun.modmanager.dummy 18 | 19 | import kotlinx.serialization.ExperimentalSerializationApi 20 | import kotlinx.serialization.decodeFromString 21 | import kotlinx.serialization.json.Json 22 | import net.minecraft.text.TranslatableText 23 | import xyz.deathsgun.modmanager.api.http.VersionResult 24 | import xyz.deathsgun.modmanager.api.mod.Asset 25 | import xyz.deathsgun.modmanager.api.mod.Version 26 | import xyz.deathsgun.modmanager.api.mod.VersionType 27 | import xyz.deathsgun.modmanager.api.provider.IModUpdateProvider 28 | import xyz.deathsgun.modmanager.providers.modrinth.models.ModrinthVersion 29 | import java.io.BufferedReader 30 | import java.io.InputStreamReader 31 | import java.time.Instant 32 | import java.time.ZoneOffset 33 | 34 | /** 35 | * Dummy which provided the version data 36 | * for [xyz.deathsgun.modmanager.update.VersionFinderTest] 37 | */ 38 | internal class DummyModrinthVersionProvider : IModUpdateProvider { 39 | 40 | private val json = Json { 41 | ignoreUnknownKeys = true 42 | } 43 | 44 | override fun getName(): String { 45 | return "Modrinth" 46 | } 47 | 48 | /** 49 | * Reads the provided id from /version/id.json 50 | * @param id the mod slug from Modrinth 51 | */ 52 | @OptIn(ExperimentalSerializationApi::class) 53 | override fun getVersionsForMod(id: String): VersionResult { 54 | return try { 55 | val stream = DummyModrinthVersionProvider::class.java.getResourceAsStream("/version/$id.json") 56 | ?: return VersionResult.Error(TranslatableText("reading.failed")) 57 | val reader = BufferedReader(InputStreamReader(stream)) 58 | val modrinthVersions = json.decodeFromString>(reader.readText()) 59 | val versions = ArrayList() 60 | for (modVersion in modrinthVersions) { 61 | if (!modVersion.loaders.contains("fabric")) { 62 | continue 63 | } 64 | val assets = ArrayList() 65 | for (file in modVersion.files) { 66 | assets.add(Asset(file.url, file.filename, file.hashes, file.primary)) 67 | } 68 | versions.add( 69 | Version( 70 | modVersion.version, 71 | modVersion.changelog, 72 | // 2021-09-03T10:56:59.402790Z 73 | Instant.parse(modVersion.releaseDate).atOffset( 74 | ZoneOffset.UTC 75 | ).toLocalDate(), 76 | getVersionType(modVersion.type), 77 | modVersion.gameVersions, 78 | assets 79 | ) 80 | ) 81 | } 82 | VersionResult.Success(versions) 83 | } catch (e: Exception) { 84 | VersionResult.Error(TranslatableText("modmanager.error.failedToParse", e.message), e) 85 | } 86 | } 87 | 88 | private fun getVersionType(id: String): VersionType { 89 | return when (id) { 90 | "release" -> VersionType.RELEASE 91 | "alpha" -> VersionType.ALPHA 92 | "beta" -> VersionType.BETA 93 | else -> VersionType.UNKNOWN 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /src/test/kotlin/xyz/deathsgun/modmanager/providers/modrinth/ModrinthTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 DeathsGun 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package xyz.deathsgun.modmanager.providers.modrinth 18 | 19 | import net.minecraft.text.TranslatableText 20 | import org.junit.jupiter.api.Test 21 | import xyz.deathsgun.modmanager.api.http.CategoriesResult 22 | import xyz.deathsgun.modmanager.api.http.ModResult 23 | import xyz.deathsgun.modmanager.api.http.ModsResult 24 | import xyz.deathsgun.modmanager.api.http.VersionResult 25 | import xyz.deathsgun.modmanager.api.mod.Category 26 | import xyz.deathsgun.modmanager.api.mod.Mod 27 | import xyz.deathsgun.modmanager.api.mod.VersionType 28 | import xyz.deathsgun.modmanager.api.provider.Sorting 29 | import kotlin.test.* 30 | 31 | internal class ModrinthTest { 32 | 33 | private val modrinth = Modrinth() 34 | 35 | @Test 36 | fun getCategories() { 37 | val result = modrinth.getCategories() 38 | if (result is CategoriesResult.Error) { 39 | result.cause?.let { 40 | fail(result.text.key, it) 41 | } 42 | fail(result.text.key) 43 | } 44 | val categories = (result as CategoriesResult.Success).categories 45 | assertTrue(categories.isNotEmpty()) 46 | categories.forEach { 47 | assertTrue(it.id.isNotEmpty()) 48 | assertEquals(String.format("modmanager.category.%s", it.id), it.text.key) 49 | } 50 | } 51 | 52 | @Test 53 | fun getModsBySorting() { 54 | val result = modrinth.getMods(Sorting.NEWEST, 0, 10) 55 | if (result is ModsResult.Error) { 56 | result.cause?.let { 57 | fail(result.text.key, it) 58 | } 59 | fail(result.text.key) 60 | } 61 | val mods = (result as ModsResult.Success).mods 62 | checkMods(mods) 63 | } 64 | 65 | @Test 66 | fun getModsByCategory() { 67 | val result = modrinth.getMods(listOf(Category("misc", TranslatableText(""))), Sorting.RELEVANCE, 0, 10) 68 | if (result is ModsResult.Error) { 69 | result.cause?.let { 70 | fail(result.text.key, it) 71 | } 72 | fail(result.text.key) 73 | } 74 | val mods = (result as ModsResult.Success).mods 75 | checkMods(mods) 76 | } 77 | 78 | @Test 79 | fun getModsByQuery() { 80 | val result = modrinth.search("Mod", emptyList(), Sorting.DOWNLOADS, 0, 10) 81 | if (result is ModsResult.Error) { 82 | result.cause?.let { 83 | fail(result.text.key, it) 84 | } 85 | fail(result.text.key) 86 | } 87 | val mods = (result as ModsResult.Success).mods 88 | checkMods(mods) 89 | } 90 | 91 | private fun checkMods(mods: List) { 92 | assertTrue(mods.isNotEmpty()) 93 | assertEquals(mods.size, 10) 94 | mods.forEach { 95 | assertTrue(it.id.isNotEmpty()) 96 | assertTrue(it.slug.isNotEmpty()) 97 | assertTrue(it.name.isNotEmpty()) 98 | assertNotNull(it.author) 99 | assertNotNull(it.iconUrl) 100 | assertTrue(it.shortDescription.isNotEmpty()) 101 | assertTrue(it.categories.isNotEmpty()) 102 | 103 | // Only filled when getMod(id) is called 104 | assertNull(it.description, "description should be null as it's only loaded by getMod") 105 | assertNull(it.license, "description should be null as it's only loaded by getMod") 106 | } 107 | } 108 | 109 | @Test 110 | fun getMod() { 111 | val testMod = modrinth.getMods(Sorting.NEWEST, 0, 1) 112 | if (testMod is ModsResult.Error) { 113 | testMod.cause?.let { 114 | fail(testMod.text.key, it) 115 | } 116 | fail(testMod.text.key) 117 | } 118 | val result = modrinth.getMod((testMod as ModsResult.Success).mods[0].id) 119 | if (result is ModResult.Error) { 120 | result.cause?.let { 121 | fail(result.text.key, it) 122 | } 123 | fail(result.text.key) 124 | } 125 | val mod = (result as ModResult.Success).mod 126 | assertTrue(mod.id.isNotEmpty()) 127 | assertTrue(mod.slug.isNotEmpty()) 128 | assertTrue(mod.name.isNotEmpty()) 129 | assertNull(mod.author) 130 | assertTrue(mod.shortDescription.isNotEmpty()) 131 | assertTrue(mod.categories.isNotEmpty()) 132 | assertNotNull(mod.description) 133 | assertTrue(mod.description!!.isNotEmpty()) 134 | assertNotNull(mod.license) 135 | assertTrue(mod.license!!.isNotEmpty()) 136 | } 137 | 138 | @Test 139 | fun getVersionsForMod() { 140 | val result = modrinth.getVersionsForMod("6kq7BzRK") 141 | if (result is VersionResult.Error) { 142 | result.cause?.let { 143 | fail(result.text.key, it) 144 | } 145 | fail(result.text.key) 146 | } 147 | val versions = (result as VersionResult.Success).versions 148 | assertTrue(versions.isNotEmpty()) 149 | versions.forEach { 150 | assertTrue(it.gameVersions.isNotEmpty()) 151 | assertTrue(it.version.isNotEmpty()) 152 | assertTrue(it.changelog.isNotEmpty()) 153 | it.assets.forEach { asset -> 154 | assertTrue(asset.filename.isNotEmpty()) 155 | assertTrue(asset.filename.endsWith(".jar")) 156 | assertTrue(asset.url.isNotEmpty()) 157 | assertTrue(asset.hashes.isNotEmpty()) 158 | assertContains(asset.hashes, "sha512") 159 | } 160 | } 161 | } 162 | } -------------------------------------------------------------------------------- /src/test/kotlin/xyz/deathsgun/modmanager/update/VersionFinderTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 DeathsGun 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package xyz.deathsgun.modmanager.update 18 | 19 | import net.fabricmc.loader.api.VersionParsingException 20 | import org.junit.jupiter.api.DynamicTest.dynamicTest 21 | import org.junit.jupiter.api.TestFactory 22 | import org.junit.jupiter.api.fail 23 | import xyz.deathsgun.modmanager.api.http.VersionResult 24 | import xyz.deathsgun.modmanager.api.provider.IModUpdateProvider 25 | import xyz.deathsgun.modmanager.config.Config 26 | import xyz.deathsgun.modmanager.dummy.DummyModrinthVersionProvider 27 | import kotlin.test.assertEquals 28 | import kotlin.test.assertNotNull 29 | import kotlin.test.assertNull 30 | import kotlin.test.assertTrue 31 | 32 | /** 33 | * Tests the [VersionFinder] implementation 34 | */ 35 | internal class VersionFinderTest { 36 | 37 | private val provider: IModUpdateProvider = DummyModrinthVersionProvider() 38 | 39 | /** 40 | * Tests the fallback for versions which are not following 41 | * the SemVer scheme. These mods are resolved by their release date. 42 | */ 43 | @TestFactory 44 | fun findUpdateByFallback() = listOf( 45 | Scenario( 46 | "lithium", 47 | "mc1.17.1-0.7.4", 48 | "mc1.17.1-0.7.1", 49 | Config.UpdateChannel.STABLE, 50 | "1.17" 51 | ), 52 | Scenario( 53 | "dynamic-fps", 54 | "v2.0.5", 55 | "2.0.5", 56 | Config.UpdateChannel.STABLE, 57 | "1.17" 58 | ), 59 | Scenario( 60 | "iris", 61 | "mc1.17.1-1.1.2", 62 | "mc1.17-v1.1.1", 63 | Config.UpdateChannel.STABLE, 64 | "1.17" 65 | ) 66 | ).map { scenario -> 67 | dynamicTest("${scenario.mod} ${scenario.expectedVersion} ${scenario.channel}") { 68 | val versions = when (val result = provider.getVersionsForMod(scenario.mod)) { 69 | is VersionResult.Error -> fail(result.text.key, result.cause) 70 | is VersionResult.Success -> result.versions 71 | } 72 | val latest = VersionFinder.findUpdateFallback( 73 | scenario.installedVersion, 74 | scenario.mcVersion, 75 | scenario.mcVersion, 76 | scenario.channel, 77 | versions 78 | ) 79 | assertNotNull(latest) 80 | assertEquals(scenario.expectedVersion, latest.version) 81 | assertTrue(latest.assets.isNotEmpty()) 82 | } 83 | } 84 | 85 | /** 86 | * Tests some version scenarios in which the [VersionFinder] 87 | * should definitely return null. 88 | */ 89 | @TestFactory 90 | fun findUpdateByVersionError() = listOf( 91 | /** 92 | * This scenario will fail because mc1.17.1-0.7.1 93 | * can not be parsed by the parser as it starts with mc. 94 | */ 95 | Scenario( 96 | "lithium", 97 | "none", 98 | "mc1.17.1-0.7.1", 99 | Config.UpdateChannel.STABLE, 100 | "1.17" 101 | ), 102 | /** 103 | * This scenario will fail because fabric-5.3.3-BETA+6027c282 104 | * can not be parsed by the parser as it starts with fabric. 105 | */ 106 | Scenario( 107 | "terra", 108 | "none", 109 | "fabric-5.3.3-BETA+6027c282", 110 | Config.UpdateChannel.ALL, 111 | "1.17" 112 | ), 113 | /** 114 | * No update for this scenario should be found. 115 | * When an update should be found this would be an 116 | * error as 2.0.13 is the latest 117 | */ 118 | Scenario( 119 | "modmenu", 120 | "none", 121 | "2.0.13", 122 | Config.UpdateChannel.STABLE, 123 | "1.17" 124 | ) 125 | ).map { scenario -> 126 | dynamicTest("${scenario.mod} ${scenario.expectedVersion} ${scenario.channel}") { 127 | val versions = when (val result = provider.getVersionsForMod(scenario.mod)) { 128 | is VersionResult.Error -> fail(result.text.key, result.cause) 129 | is VersionResult.Success -> result.versions 130 | } 131 | val latest = try { 132 | VersionFinder.findUpdateByVersion( 133 | scenario.installedVersion, 134 | scenario.mcVersion, 135 | scenario.mcVersion, 136 | scenario.channel, 137 | versions 138 | ) 139 | } catch (e: VersionParsingException) { 140 | null 141 | } 142 | assertNull(latest) 143 | } 144 | } 145 | 146 | /** 147 | * This tests all mods which are following the semantic version scheme. 148 | */ 149 | @TestFactory 150 | fun findUpdateByVersion() = listOf( 151 | Scenario( 152 | "modmanager", 153 | "1.0.2+1.17-alpha", 154 | "1.0.0-alpha", 155 | Config.UpdateChannel.ALL, 156 | "1.17" 157 | ), 158 | Scenario( 159 | "modmenu", 160 | "2.0.13", 161 | "2.0.5", 162 | Config.UpdateChannel.ALL, 163 | "1.17" 164 | ) 165 | ).map { scenario -> 166 | dynamicTest("${scenario.mod} ${scenario.expectedVersion} ${scenario.channel}") { 167 | val versions = when (val result = provider.getVersionsForMod(scenario.mod)) { 168 | is VersionResult.Error -> fail(result.text.key, result.cause) 169 | is VersionResult.Success -> result.versions 170 | } 171 | val latest = VersionFinder.findUpdateByVersion( 172 | scenario.installedVersion, 173 | scenario.mcVersion, 174 | scenario.mcVersion, 175 | scenario.channel, 176 | versions 177 | ) 178 | assertNotNull(latest) 179 | assertEquals(scenario.expectedVersion, latest.version) 180 | } 181 | } 182 | 183 | private data class Scenario( 184 | val mod: String, 185 | val expectedVersion: String, 186 | val installedVersion: String, 187 | val channel: Config.UpdateChannel, 188 | val mcVersion: String 189 | ) 190 | } -------------------------------------------------------------------------------- /src/test/resources/version/dynamic-fps.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "RgJGw0dO", 4 | "mod_id": "LQ3K71Q1", 5 | "author_id": "HLI9Dbyv", 6 | "featured": false, 7 | "name": "v2.0.5", 8 | "version_number": "v2.0.5", 9 | "changelog": "- Worked around reload screen fadeout not working (#35—thanks, altrisi!)\n- File size reduced by ~300 KB (#30—thanks, wafflecoffee!)\n- Localization updates: Russian (#28), Chinese (#34)", 10 | "changelog_url": null, 11 | "date_published": "2021-08-08T21:03:25.103818Z", 12 | "downloads": 2929, 13 | "version_type": "release", 14 | "files": [ 15 | { 16 | "hashes": { 17 | "sha512": "84135f5da20608c732ac90439dd7929dcf61cc6471667e5c8842466ec0cbfdf3199d8dc2cdb2088b5ba76cde55c5df015989b00376d6be65c4b8a1c1d3b13be7", 18 | "sha1": "07eab9095517baedb1ba8426310e7fa66fd9747d" 19 | }, 20 | "url": "https://cdn.modrinth.com/data/LQ3K71Q1/versions/v2.0.5/dynamic-fps-2.0.5.jar", 21 | "filename": "dynamic-fps-2.0.5.jar", 22 | "primary": false 23 | } 24 | ], 25 | "dependencies": [], 26 | "game_versions": [ 27 | "1.17.1" 28 | ], 29 | "loaders": [ 30 | "fabric" 31 | ] 32 | }, 33 | { 34 | "id": "oIZUkvvs", 35 | "mod_id": "LQ3K71Q1", 36 | "author_id": "HLI9Dbyv", 37 | "featured": false, 38 | "name": "2.0.4", 39 | "version_number": "v2.0.4", 40 | "changelog": "- Added an option to run garbage collection whenever the window loses focus.", 41 | "changelog_url": null, 42 | "date_published": "2021-06-29T12:56:14.536461Z", 43 | "downloads": 1300, 44 | "version_type": "release", 45 | "files": [ 46 | { 47 | "hashes": { 48 | "sha1": "a172c61df0a4deffa669d19f7dd83282b2ea0210", 49 | "sha512": "539617d9f1b32fc4ef3859ec6c7cf934de20552b0f3255b8881213c8496fbcb7d646375fd5558cf173d8ce7db9e97add9f2f49b3dd05ee181f6334d14b9c666f" 50 | }, 51 | "url": "https://cdn.modrinth.com/data/LQ3K71Q1/versions/v2.0.4/dynamic-fps-2.0.4.jar", 52 | "filename": "dynamic-fps-2.0.4.jar", 53 | "primary": true 54 | } 55 | ], 56 | "dependencies": [], 57 | "game_versions": [ 58 | "1.17", 59 | "1.17.1-pre1" 60 | ], 61 | "loaders": [ 62 | "fabric" 63 | ] 64 | }, 65 | { 66 | "id": "muZxaaxq", 67 | "mod_id": "LQ3K71Q1", 68 | "author_id": "HLI9Dbyv", 69 | "featured": false, 70 | "name": "2.0.2", 71 | "version_number": "v2.0.2", 72 | "changelog": "- Updated Russian Localization ([#22—Felix14-v2](https://github.com/juliand665/Dynamic-FPS/pull/22))", 73 | "changelog_url": null, 74 | "date_published": "2021-05-08T14:35:25.087402Z", 75 | "downloads": 1524, 76 | "version_type": "release", 77 | "files": [ 78 | { 79 | "hashes": { 80 | "sha1": "1b080f859e529dbe87a0b5d7c71adfbb6bca5db4", 81 | "sha512": "b3a8a9e008a3cf6c585c3ddd016374f6e202e8f321259c7c2cd014936884d4658ad5ef0de685e0cee92d51840c12ef8a5438efc2a7e7ffaf4dfe3745dad293d5" 82 | }, 83 | "url": "https://cdn.modrinth.com/data/LQ3K71Q1/versions/2.0.2/dynamic-fps-2.0.2.jar", 84 | "filename": "dynamic-fps-2.0.2.jar", 85 | "primary": false 86 | } 87 | ], 88 | "dependencies": [], 89 | "game_versions": [ 90 | "1.16.5", 91 | "21w03a", 92 | "21w05a", 93 | "21w05b", 94 | "21w06a", 95 | "21w07a", 96 | "21w08a", 97 | "21w08b", 98 | "21w10a", 99 | "21w11a", 100 | "21w13a", 101 | "21w14a", 102 | "21w15a", 103 | "21w16a", 104 | "21w17a", 105 | "21w18a" 106 | ], 107 | "loaders": [ 108 | "fabric" 109 | ] 110 | }, 111 | { 112 | "id": "XlBOTUIQ", 113 | "mod_id": "LQ3K71Q1", 114 | "author_id": "HLI9Dbyv", 115 | "featured": false, 116 | "name": "Localization", 117 | "version_number": "v2.0.1", 118 | "changelog": "", 119 | "changelog_url": null, 120 | "date_published": "2021-01-21T04:25:08.291787Z", 121 | "downloads": 772, 122 | "version_type": "release", 123 | "files": [ 124 | { 125 | "hashes": { 126 | "sha512": "923eb8206fea45bfc455bb195d71ab3eddf986e1cc2cc48fa9543e7601b34c0e184ad463f4c119622218559a7872ea4e3ebc6815c2dc172f41d1cabe1171ffa2", 127 | "sha1": "672904fc5332f7064db872635549b63a10444f59" 128 | }, 129 | "url": "https://cdn.modrinth.com/data/LQ3K71Q1/versions/v2.0.1/dynamic-fps-2.0.1.jar", 130 | "filename": "dynamic-fps-2.0.1.jar", 131 | "primary": false 132 | } 133 | ], 134 | "dependencies": [], 135 | "game_versions": [ 136 | "1.16.2", 137 | "1.16.3", 138 | "1.16.3-rc1", 139 | "1.16.4", 140 | "1.16.4-pre1", 141 | "1.16.4-pre2", 142 | "1.16.4-rc1", 143 | "1.16.5", 144 | "1.16.5-rc1", 145 | "20w45a", 146 | "20w46a", 147 | "20w48a", 148 | "20w49a", 149 | "20w51a", 150 | "21w03a" 151 | ], 152 | "loaders": [ 153 | "fabric" 154 | ] 155 | }, 156 | { 157 | "id": "gVvtLF6M", 158 | "mod_id": "LQ3K71Q1", 159 | "author_id": "HLI9Dbyv", 160 | "featured": false, 161 | "name": "Configurability", 162 | "version_number": "v2.0.0", 163 | "changelog": "", 164 | "changelog_url": null, 165 | "date_published": "2021-01-19T16:53:49.879150Z", 166 | "downloads": 28, 167 | "version_type": "release", 168 | "files": [ 169 | { 170 | "hashes": { 171 | "sha1": "972a09c66b164dd77e97524e06fc16904a859863", 172 | "sha512": "38e85da3025c9b47905781fae0f4ae10a2437e97716c3fa60bb04510c57154e14e3cbd17a6ee5a8b375ee66621010a0110f9784709abd3a7fbdbbb5220be0be7" 173 | }, 174 | "url": "https://cdn.modrinth.com/data/LQ3K71Q1/versions/v2.0.0/dynamic-fps-2.0.0.jar", 175 | "filename": "dynamic-fps-2.0.0.jar", 176 | "primary": false 177 | } 178 | ], 179 | "dependencies": [], 180 | "game_versions": [ 181 | "1.16.2", 182 | "1.16.3", 183 | "1.16.3-rc1", 184 | "1.16.4", 185 | "1.16.4-pre1", 186 | "1.16.4-pre2", 187 | "1.16.4-rc1", 188 | "1.16.5", 189 | "1.16.5-rc1", 190 | "20w45a", 191 | "20w46a", 192 | "20w48a", 193 | "20w49a", 194 | "20w51a" 195 | ], 196 | "loaders": [ 197 | "fabric" 198 | ] 199 | }, 200 | { 201 | "id": "kurPEwi6", 202 | "mod_id": "LQ3K71Q1", 203 | "author_id": "HLI9Dbyv", 204 | "featured": false, 205 | "name": "Localization", 206 | "version_number": "v1.2.1", 207 | "changelog": "", 208 | "changelog_url": "https://cdn.modrinth.com/data/LQ3K71Q1/versions/v1.2.1/changelog.md", 209 | "date_published": "2021-01-07T18:37:47.131931Z", 210 | "downloads": 17, 211 | "version_type": "release", 212 | "files": [ 213 | { 214 | "hashes": { 215 | "sha1": "2103fd47a4ee86087e908df34eacdcb0bc271d09" 216 | }, 217 | "url": "https://cdn.modrinth.com/data/LQ3K71Q1/versions/v1.2.1/dynamic-fps-1.2.1.jar", 218 | "filename": "dynamic-fps-1.2.1.jar", 219 | "primary": false 220 | } 221 | ], 222 | "dependencies": [], 223 | "game_versions": [ 224 | "1.16.2", 225 | "1.16.2-pre1", 226 | "1.16.2-pre2", 227 | "1.16.2-pre3", 228 | "1.16.2-rc1", 229 | "1.16.2-rc2", 230 | "1.16.3", 231 | "1.16.3-rc1", 232 | "1.16.4", 233 | "1.16.4-pre1", 234 | "1.16.4-pre2", 235 | "1.16.4-rc1", 236 | "20w45a", 237 | "20w46a", 238 | "20w48a", 239 | "20w49a", 240 | "20w51a" 241 | ], 242 | "loaders": [ 243 | "fabric" 244 | ] 245 | } 246 | ] -------------------------------------------------------------------------------- /src/test/resources/version/modmanager.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "CdhVgHal", 4 | "mod_id": "6kq7BzRK", 5 | "author_id": "VUdG1FR5", 6 | "featured": false, 7 | "name": "1.0.2+1.17-alpha", 8 | "version_number": "1.0.2+1.17-alpha", 9 | "changelog": "Bugs fixed:\n\n- Fix CPU overload when using ModManager [#48](https://github.com/DeathsGun/ModManager/issues/48)\n- Fix forge artifacts being downloaded [#37](https://github.com/DeathsGun/ModManager/pull/37)\n- Fix NullPointerException while updating a mod [#34](https://github.com/DeathsGun/ModManager/issues/34)\n\nImprovements:\n\n- New loading icon [#40](https://github.com/DeathsGun/ModManager/pull/40)\n- Improved Turkish translation (Special thanks to kuzeeeyk) [#39](https://github.com/DeathsGun/ModManager/pull/39)\n- Added Chinese translation (Special thanks to MineCommanderCN) [#36](https://github.com/DeathsGun/ModManager/pull/36)\n- Added Korean translation (Special thanks to arlytical#1) [#32](https://github.com/DeathsGun/ModManager/pull/32)\n- Added Russian translation (Special thanks to Felix14-v2) [#31](https://github.com/DeathsGun/ModManager/pull/31)\n\n", 10 | "changelog_url": null, 11 | "date_published": "2021-09-03T10:56:59.402790Z", 12 | "downloads": 474, 13 | "version_type": "alpha", 14 | "files": [ 15 | { 16 | "hashes": { 17 | "sha512": "24661776bab56c8e77abaac3c66219e01742de2a0e9433945bfd1a6d284b1dd41e891f1d9c62f30c8f34688525e2c784e84e8720bde9355b2d7efa5e4fb53d2d", 18 | "sha1": "c1b802582903dadf56c1965bc5c6a31716250848" 19 | }, 20 | "url": "https://cdn.modrinth.com//data/6kq7BzRK/versions/1.0.2+1.17-alpha/modmanager-1.0.2+1.17-alpha.jar", 21 | "filename": "modmanager-1.0.2+1.17-alpha.jar", 22 | "primary": true 23 | } 24 | ], 25 | "dependencies": [ 26 | { 27 | "version_id": "E4QBMVtO", 28 | "dependency_type": "required" 29 | } 30 | ], 31 | "game_versions": [ 32 | "1.17.1" 33 | ], 34 | "loaders": [ 35 | "fabric" 36 | ] 37 | }, 38 | { 39 | "id": "gGSLHqJK", 40 | "mod_id": "6kq7BzRK", 41 | "author_id": "VUdG1FR5", 42 | "featured": false, 43 | "name": "1.0.1+1.17-alpha", 44 | "version_number": "1.0.1+1.17-alpha", 45 | "changelog": "Bugs fixed:\n\n- Fixed crash when opening ModMenu [#13](https://github.com/DeathsGun/ModManager/issues/13)\n- Fixed update error on Windows because of file locks [#17](https://github.com/DeathsGun/ModManager/issues/13)\n- Search only when enter key was hit for improved performance [#7](https://github.com/DeathsGun/ModManager/issues/7)\n- Fixed crash when ModManager loses connection while opening a more detailed\n view [#16](https://github.com/DeathsGun/ModManager/issues/16)\n- Fixed icons being mixed up [#22](https://github.com/DeathsGun/ModManager/issues/22)\n- Fixed unknown mods showing up [#18](https://github.com/DeathsGun/ModManager/issues/18)\n\nImprovements:\n\n- Added Turkish translation (Special thanks to kuzeeeyk) [#21](https://github.com/DeathsGun/ModManager/pull/21)\n- Only show \"Updatable mods\" category when there are updatable\n mods [#10](https://github.com/DeathsGun/ModManager/issues/10)\n", 46 | "changelog_url": null, 47 | "date_published": "2021-08-24T17:26:03.297706Z", 48 | "downloads": 225, 49 | "version_type": "alpha", 50 | "files": [ 51 | { 52 | "hashes": { 53 | "sha1": "0228ab2ac3cc3120512041d01b31925f03e764e5", 54 | "sha512": "7ec3293ac8f43adf12e02e60f3ce1c1a6410fb21674bf8ab50184d4e32cf34535b428f642b429515c7119328d49d0c852817cb00f7c109cc1135963313c9d474" 55 | }, 56 | "url": "https://cdn.modrinth.com/data/6kq7BzRK/versions/1.0.1+1.17-alpha/modmanager-1.0.1+1.17-alpha.jar", 57 | "filename": "modmanager-1.0.1+1.17-alpha.jar", 58 | "primary": true 59 | } 60 | ], 61 | "dependencies": [ 62 | { 63 | "version_id": "E4QBMVtO", 64 | "dependency_type": "required" 65 | } 66 | ], 67 | "game_versions": [ 68 | "1.17.1" 69 | ], 70 | "loaders": [ 71 | "fabric" 72 | ] 73 | }, 74 | { 75 | "id": "2J1ys6qf", 76 | "mod_id": "6kq7BzRK", 77 | "author_id": "VUdG1FR5", 78 | "featured": false, 79 | "name": "1.0.0-alpha", 80 | "version_number": "1.0.0-alpha", 81 | "changelog": "This project has reached the alpha stage.\nI think the mod has reached a point in which it can be called an alpha.\n\nWhat can do now?\n* Browse through Modrinth\n* Install, remove and update mods\n* Notify about updates\n* Show you an overview of mods that need an update ", 82 | "changelog_url": null, 83 | "date_published": "2021-08-23T17:00:19.010541Z", 84 | "downloads": 55, 85 | "version_type": "alpha", 86 | "files": [ 87 | { 88 | "hashes": { 89 | "sha512": "e57fddfc3d2d7789f6b260e345ba5a469b0de343692535d742762336afaf10d0f753aa537384d48b7aa16a5c6a4d71adff36b22f26f6248862f19f94dfbcd5ce", 90 | "sha1": "a646cc8a6ef18ad2bf8a771776780e87d35e79a1" 91 | }, 92 | "url": "https://cdn.modrinth.com/data/6kq7BzRK/versions/1.0.0-alpha/modmanager-1.0.0-alpha.jar", 93 | "filename": "modmanager-1.0.0-alpha.jar", 94 | "primary": true 95 | } 96 | ], 97 | "dependencies": [ 98 | { 99 | "version_id": "E4QBMVtO", 100 | "dependency_type": "required" 101 | } 102 | ], 103 | "game_versions": [ 104 | "1.17.1" 105 | ], 106 | "loaders": [ 107 | "fabric" 108 | ] 109 | } 110 | ] --------------------------------------------------------------------------------