├── app ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── ic_launcher-playstore.png │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── layout │ │ │ │ ├── activity_debug.xml │ │ │ │ ├── fragment_project_downloads.xml │ │ │ │ ├── fragment_project_description.xml │ │ │ │ ├── activity_main.xml │ │ │ │ ├── fragment_list.xml │ │ │ │ ├── download_item.xml │ │ │ │ ├── fragment_settings_container.xml │ │ │ │ ├── drawer_header.xml │ │ │ │ ├── list_item.xml │ │ │ │ ├── list_downloads.xml │ │ │ │ ├── fragment_home.xml │ │ │ │ ├── fragment_project_view.xml │ │ │ │ ├── activity_auth.xml │ │ │ │ ├── fragment_user.xml │ │ │ │ └── fragment_project_info.xml │ │ │ ├── xml │ │ │ │ ├── backup_rules.xml │ │ │ │ ├── data_extraction_rules.xml │ │ │ │ └── root_preferences.xml │ │ │ ├── drawable │ │ │ │ ├── ic_texture.xml │ │ │ │ ├── ic_edit.xml │ │ │ │ ├── ic_arrow_back.xml │ │ │ │ ├── ic_search.xml │ │ │ │ ├── ic_menu.xml │ │ │ │ ├── ic_download.xml │ │ │ │ ├── ic_description.xml │ │ │ │ ├── ic_inventory_2.xml │ │ │ │ ├── ic_info.xml │ │ │ │ ├── ic_build.xml │ │ │ │ ├── ic_person.xml │ │ │ │ ├── ic_update.xml │ │ │ │ ├── ic_launcher_foreground.xml │ │ │ │ ├── ic_share.xml │ │ │ │ ├── ic_extension.xml │ │ │ │ └── ic_settings.xml │ │ │ ├── menu │ │ │ │ ├── main_menu.xml │ │ │ │ ├── toolbar_menu_user.xml │ │ │ │ ├── project_view_menu.xml │ │ │ │ └── toolbar_menu.xml │ │ │ ├── values │ │ │ │ ├── arrays.xml │ │ │ │ ├── colors.xml │ │ │ │ ├── strings.xml │ │ │ │ └── themes.xml │ │ │ ├── values-night │ │ │ │ └── themes.xml │ │ │ └── raw │ │ │ │ └── licenses.txt │ │ ├── java │ │ │ └── me │ │ │ │ └── theclashfruit │ │ │ │ └── rithle │ │ │ │ ├── models │ │ │ │ ├── ModrinthModMessageModel.kt │ │ │ │ ├── ModrinthLicenseModel.kt │ │ │ │ ├── ModrinthDonationUrlsModel.kt │ │ │ │ ├── GitHubAccessTokenModel.kt │ │ │ │ ├── ModrinthGalleryModel.kt │ │ │ │ ├── ModrinthSearchModel.kt │ │ │ │ ├── ModrinthSearchHitsModel.kt │ │ │ │ └── ModrinthProjectModel.kt │ │ │ │ ├── classes │ │ │ │ ├── ProxySchemeHandler.kt │ │ │ │ ├── MrApiUrlUtil.kt │ │ │ │ ├── ListDiffCallback.kt │ │ │ │ ├── RithleSingleton.kt │ │ │ │ └── FilterBuilder.kt │ │ │ │ ├── RithleApplication.kt │ │ │ │ ├── fragments │ │ │ │ ├── SettingsFragment.kt │ │ │ │ ├── SettingsContainerFragment.kt │ │ │ │ ├── ProjectDescriptionFragment.kt │ │ │ │ ├── ProjectDownloadsFragment.kt │ │ │ │ ├── ListFragment.kt │ │ │ │ ├── ProjectViewFragment.kt │ │ │ │ ├── UserFragment.kt │ │ │ │ ├── HomeFragment.kt │ │ │ │ └── ProjectInfoFragment.kt │ │ │ │ ├── MainActivity.kt │ │ │ │ ├── adapters │ │ │ │ ├── ModListAdapter.kt │ │ │ │ └── DownloadsAdapter.kt │ │ │ │ ├── services │ │ │ │ └── NotificationService.kt │ │ │ │ └── AuthActivity.kt │ │ └── AndroidManifest.xml │ ├── test │ │ └── java │ │ │ └── me │ │ │ └── theclashfruit │ │ │ └── rithle │ │ │ └── ExampleUnitTest.kt │ └── androidTest │ │ └── java │ │ └── me │ │ └── theclashfruit │ │ └── rithle │ │ └── ExampleInstrumentedTest.kt ├── proguard-rules.pro └── build.gradle ├── .idea ├── .gitignore ├── codeStyles │ ├── codeStyleConfig.xml │ └── Project.xml ├── compiler.xml ├── vcs.xml ├── discord.xml ├── gradle.xml └── misc.xml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .github ├── ISSUE_TEMPLATE │ ├── other.md │ ├── feature_request.md │ └── bug_report.md ├── FUNDING.yml └── workflows │ └── android.yml ├── .gitignore ├── settings.gradle ├── CONTRIBUTING.md ├── gradle.properties ├── gradlew.bat ├── README.md └── gradlew /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheClashFruit/Rithle/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/res/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheClashFruit/Rithle/HEAD/app/src/main/res/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheClashFruit/Rithle/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheClashFruit/Rithle/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheClashFruit/Rithle/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheClashFruit/Rithle/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheClashFruit/Rithle/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheClashFruit/Rithle/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheClashFruit/Rithle/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheClashFruit/Rithle/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/other.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Other 3 | about: Anything other. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheClashFruit/Rithle/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheClashFruit/Rithle/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/discord.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Nov 23 13:54:36 CET 2022 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /app/src/main/java/me/theclashfruit/rithle/models/ModrinthModMessageModel.kt: -------------------------------------------------------------------------------- 1 | package me.theclashfruit.rithle.models 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class ModrinthModMessageModel( 7 | var message : String? = null, 8 | var body : String? = null 9 | ) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/java/me/theclashfruit/rithle/models/ModrinthLicenseModel.kt: -------------------------------------------------------------------------------- 1 | package me.theclashfruit.rithle.models 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class ModrinthLicenseModel ( 7 | var id : String? = null, 8 | var name : String? = null, 9 | var url : String? = null 10 | ) -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/java/me/theclashfruit/rithle/models/ModrinthDonationUrlsModel.kt: -------------------------------------------------------------------------------- 1 | package me.theclashfruit.rithle.models 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class ModrinthDonationUrlsModel ( 7 | var id : String? = null, 8 | var platform : String? = null, 9 | var url : String? = null 10 | ) -------------------------------------------------------------------------------- /app/src/main/java/me/theclashfruit/rithle/models/GitHubAccessTokenModel.kt: -------------------------------------------------------------------------------- 1 | package me.theclashfruit.rithle.models 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class GitHubAccessTokenModel ( 7 | var access_token : String? = null, 8 | var token_type : String? = null, 9 | var scope : String? = null 10 | ) -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | google() 5 | mavenCentral() 6 | } 7 | } 8 | dependencyResolutionManagement { 9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 10 | repositories { 11 | google() 12 | mavenCentral() 13 | } 14 | } 15 | rootProject.name = "Rithle" 16 | include ':app' 17 | -------------------------------------------------------------------------------- /app/src/main/java/me/theclashfruit/rithle/models/ModrinthGalleryModel.kt: -------------------------------------------------------------------------------- 1 | package me.theclashfruit.rithle.models 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class ModrinthGalleryModel ( 7 | var url : String? = null, 8 | var featured : Boolean? = null, 9 | var title : String? = null, 10 | var description : String? = null, 11 | var created : String? = null 12 | ) -------------------------------------------------------------------------------- /app/src/main/java/me/theclashfruit/rithle/classes/ProxySchemeHandler.kt: -------------------------------------------------------------------------------- 1 | package me.theclashfruit.rithle.classes 2 | 3 | import android.net.Uri 4 | import io.noties.markwon.image.ImageItem 5 | import io.noties.markwon.image.SchemeHandler 6 | 7 | abstract class ProxySchemeHandler : SchemeHandler() { 8 | abstract override fun handle(raw: String, uri: Uri): ImageItem 9 | abstract override fun supportedSchemes(): Collection 10 | } 11 | -------------------------------------------------------------------------------- /app/src/test/java/me/theclashfruit/rithle/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package me.theclashfruit.rithle 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_debug.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_project_downloads.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 12 | -------------------------------------------------------------------------------- /app/src/main/java/me/theclashfruit/rithle/classes/MrApiUrlUtil.kt: -------------------------------------------------------------------------------- 1 | package me.theclashfruit.rithle.classes 2 | 3 | import me.theclashfruit.rithle.BuildConfig 4 | 5 | class MrApiUrlUtil { 6 | private val stableUrl = "https://api.modrinth.com" 7 | private val stagingUrl = "https://staging-api.modrinth.com" 8 | 9 | fun getApiUrl(): String { 10 | return if (BuildConfig.DEBUG) { 11 | stagingUrl 12 | } else { 13 | stableUrl 14 | } 15 | } 16 | 17 | fun getIsDebugMode(): Boolean { 18 | return BuildConfig.DEBUG 19 | } 20 | } -------------------------------------------------------------------------------- /app/src/main/java/me/theclashfruit/rithle/models/ModrinthSearchModel.kt: -------------------------------------------------------------------------------- 1 | package me.theclashfruit.rithle.models 2 | 3 | import com.google.gson.annotations.SerializedName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class ModrinthSearchModel( 8 | @SerializedName("hits" ) var hits : ArrayList = arrayListOf(), 9 | @SerializedName("offset" ) var offset : Int? = null, 10 | @SerializedName("limit" ) var limit : Int? = null, 11 | @SerializedName("total_hits" ) var totalHits : Int? = null 12 | ) -------------------------------------------------------------------------------- /app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_project_description.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 15 | -------------------------------------------------------------------------------- /.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: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_texture.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_edit.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/menu/main_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 12 | 16 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/menu/toolbar_menu_user.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 13 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/values/arrays.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | EUR 4 | GBP 5 | AED 6 | AFN 7 | AMD 8 | ALL 9 | ANG 10 | HUF 11 | 12 | 13 | 14 | 15 | 16 | 0.942 17 | 0.830 18 | 3.6726 19 | 85.300 20 | 379.670 21 | 106.875 22 | 1.790 23 | 376.301 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_back.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/androidTest/java/me/theclashfruit/rithle/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package me.theclashfruit.rithle 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("me.theclashfruit.rithle", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_search.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 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 | **Smartphone (please complete the following information):** 27 | - Device: [e.g. Samsung Galaxy A52s 5G] 28 | - OS: [e.g. Android 12] 29 | - Version [e.g. 1.3.4-stable] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [ TheClashFruit ] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: TheClashFruit # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_menu.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/menu/project_view_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 11 | 15 | 19 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #1bd96a 4 | #1bd96a 5 | 6 | #00af5c 7 | #00af5c 8 | 9 | #4f9cff 10 | #4f9cff 11 | 12 | #e5e7eb 13 | #16181C 14 | 15 | #ffffff 16 | #26292f 17 | 18 | #FF000000 19 | #99000000 20 | #FFFFFFFF 21 | #99FFFFFF 22 | 23 | #16181C 24 | -------------------------------------------------------------------------------- /app/src/main/java/me/theclashfruit/rithle/classes/ListDiffCallback.kt: -------------------------------------------------------------------------------- 1 | package me.theclashfruit.rithle.classes 2 | 3 | import androidx.recyclerview.widget.DiffUtil 4 | import me.theclashfruit.rithle.models.ModrinthSearchHitsModel 5 | 6 | class ListDiffCallback constructor(private val oldList: ArrayList?, private val newList: ArrayList?) : DiffUtil.Callback() { 7 | override fun getOldListSize(): Int { 8 | return oldList!!.size 9 | } 10 | 11 | override fun getNewListSize(): Int { 12 | return newList!!.size 13 | } 14 | 15 | override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { 16 | return oldList!![oldItemPosition].project_id == newList!![newItemPosition].project_id 17 | } 18 | 19 | override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { 20 | return oldList!![oldItemPosition].title.equals(newList!![newItemPosition].title) 21 | } 22 | } -------------------------------------------------------------------------------- /app/src/main/res/menu/toolbar_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 11 | 18 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_download.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | name: Android CI 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main", "*-dev" ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: set up JDK 11 18 | uses: actions/setup-java@v3 19 | with: 20 | java-version: '11' 21 | distribution: 'temurin' 22 | cache: gradle 23 | - name: Grant execute permission for Gradlew 24 | run: chmod +x gradlew 25 | - name: Add Oauth tokens to local.properties 26 | run: | 27 | echo ghclient=\"$GHO_CLIENT\" >> local.properties 28 | echo ghsecret=\"$GHO_SECRET\" >> local.properties 29 | env: 30 | GHO_SECRET: ${{ secrets.GHO_SECRET }} 31 | GHO_CLIENT: ${{ secrets.GHO_CLIENT }} 32 | - name: Build with Gradle 33 | run: ./gradlew assembleRelease 34 | - name: Upload APK artifacts 35 | uses: actions/upload-artifact@v3 36 | with: 37 | name: APK artifacts 38 | path: app/build/outputs/apk/release 39 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_description.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_inventory_2.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Branches 4 | 5 | - `main` Stable release of app, do not commit your changes here 6 | - `dev` Development branch, commit your changes here 7 | 8 | ## Commit Messages 9 | 10 | Please do try to follow the committing style if you wanted to contribute, it makes the repository much more clean and consistent. 11 | - Do not squash commits 12 | - Explain what the commit does or what did you do in the commit message with less than 50 characters 13 | - Use the commit description if you can't fit what you did in the commit message 14 | - Use one of these prefixes in your commit messages: 15 | - `fix` When you fixed a bug or maybe a flaw within the codebase 16 | - `feat` When you added something within the codebase (can be anything) 17 | - `tweak` When you do a little tweak in the codebase, like a tiny UI change 18 | - `chore` When you do fix something, but it doesn't affect the app in terms of functionality 19 | - `refactor` When you refactored the code, like cleaning up the code 20 | 21 | ## Pull Requests 22 | 23 | - Please be descriptive with you pull request 24 | - Please do not ping anyone in your pull request, it will not make the reviewing process faster 25 | - Please do not reopen your pull request without any changes if it has been denied -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_info.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 15 | 16 | 22 | 23 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/res/layout/download_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 16 | 17 | 29 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_build.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_person.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_update.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/java/me/theclashfruit/rithle/RithleApplication.kt: -------------------------------------------------------------------------------- 1 | package me.theclashfruit.rithle 2 | 3 | import android.app.Application 4 | import android.os.Process 5 | import android.util.Log 6 | import java.io.BufferedWriter 7 | import java.io.File 8 | import java.io.FileWriter 9 | import java.io.IOException 10 | import java.text.SimpleDateFormat 11 | import java.util.* 12 | import kotlin.system.exitProcess 13 | 14 | 15 | class RithleApplication : Application() { 16 | override fun onCreate() { 17 | super.onCreate() 18 | 19 | initializeExceptionHandler() 20 | 21 | val logDelete = File(applicationContext.dataDir.toString() + "/files/latest.log") 22 | if (logDelete.exists()) { 23 | val newLogFile = File(applicationContext.dataDir.toString() + "/files/" + SimpleDateFormat("yyyy-MM-dd_HH.mm.ss").format(Date()) + ".log") 24 | 25 | newLogFile.createNewFile() 26 | newLogFile.writeText(logDelete.readText()) 27 | 28 | logDelete.delete() 29 | } 30 | } 31 | 32 | private fun initializeExceptionHandler() { 33 | Thread.setDefaultUncaughtExceptionHandler { _, ex -> 34 | Log.e("RithleApplication", ex.stackTraceToString()) 35 | 36 | val newLogFile = File(applicationContext.dataDir.toString() + "/files/latest.log") 37 | 38 | newLogFile.writeText("------ BEGINNING OF CRASH\n" + ex.stackTraceToString() + "\n------------------") 39 | 40 | Process.killProcess(Process.myPid()) 41 | exitProcess(1) 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /app/src/main/java/me/theclashfruit/rithle/classes/RithleSingleton.kt: -------------------------------------------------------------------------------- 1 | package me.theclashfruit.rithle.classes 2 | 3 | import android.content.Context 4 | import android.graphics.Bitmap 5 | import android.util.LruCache 6 | import com.android.volley.Request 7 | import com.android.volley.RequestQueue 8 | import com.android.volley.toolbox.ImageLoader 9 | import com.android.volley.toolbox.Volley 10 | 11 | class RithleSingleton constructor(context: Context) { 12 | companion object { 13 | @Volatile 14 | private var INSTANCE: RithleSingleton? = null 15 | fun getInstance(context: Context) = 16 | INSTANCE ?: synchronized(this) { 17 | INSTANCE ?: RithleSingleton(context).also { 18 | INSTANCE = it 19 | } 20 | } 21 | } 22 | 23 | val imageLoader: ImageLoader by lazy { 24 | ImageLoader(requestQueue, 25 | object : ImageLoader.ImageCache { 26 | private val cache = LruCache(1024) 27 | override fun getBitmap(url: String): Bitmap? { 28 | return cache.get(url) 29 | } 30 | override fun putBitmap(url: String, bitmap: Bitmap) { 31 | cache.put(url, bitmap) 32 | } 33 | }) 34 | } 35 | 36 | private val requestQueue: RequestQueue by lazy { 37 | Volley.newRequestQueue(context.applicationContext) 38 | } 39 | 40 | fun addToRequestQueue(req: Request) { 41 | requestQueue.add(req) 42 | } 43 | } -------------------------------------------------------------------------------- /app/src/main/java/me/theclashfruit/rithle/fragments/SettingsFragment.kt: -------------------------------------------------------------------------------- 1 | package me.theclashfruit.rithle.fragments 2 | 3 | import android.os.Bundle 4 | import android.util.Log 5 | import androidx.appcompat.app.AppCompatDelegate 6 | import androidx.preference.CheckBoxPreference 7 | import androidx.preference.Preference 8 | import androidx.preference.PreferenceFragmentCompat 9 | import com.google.android.material.dialog.MaterialAlertDialogBuilder 10 | import me.theclashfruit.rithle.R 11 | 12 | class SettingsFragment : PreferenceFragmentCompat() { 13 | 14 | override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { 15 | setPreferencesFromResource(R.xml.root_preferences, rootKey) 16 | 17 | val licensePref = findPreference("licensesItem") 18 | val darkModePRef = findPreference("darkMode") 19 | 20 | licensePref!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { 21 | R.raw.licenses.toString() 22 | 23 | MaterialAlertDialogBuilder(requireContext()) 24 | .setTitle("Licenses") 25 | .setMessage(requireContext().resources.openRawResource(R.raw.licenses).readBytes().decodeToString()) 26 | .setPositiveButton("Ok") { _, _ -> } 27 | .show() 28 | 29 | return@OnPreferenceClickListener true 30 | } 31 | 32 | darkModePRef!!.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { p, v -> 33 | return@OnPreferenceChangeListener true 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /app/src/main/java/me/theclashfruit/rithle/classes/FilterBuilder.kt: -------------------------------------------------------------------------------- 1 | package me.theclashfruit.rithle.classes 2 | 3 | class FilterBuilder { 4 | private val filterData: ArrayList> = arrayListOf(arrayListOf()) 5 | 6 | fun addFilterItem(filter: String): FilterBuilder { 7 | filterData[0].add( 8 | filter 9 | .replace("'", "%27") 10 | .replace(":", "%3A") 11 | ) 12 | 13 | return this 14 | } 15 | 16 | fun setProjectType(projectType: String): FilterBuilder { 17 | when(projectType) { 18 | "mod" -> filterData.add(arrayListOf("project_type:$projectType")) 19 | "modpack" -> filterData.add(arrayListOf("project_type:$projectType")) 20 | "resourcepack" -> filterData.add(arrayListOf("project_type:$projectType")) 21 | else -> throw NoSuchFieldError("Invalid Project Type!") 22 | } 23 | 24 | return this 25 | } 26 | 27 | fun build(): String { 28 | val fLick: ArrayList = arrayListOf() 29 | 30 | if(filterData[0].isEmpty()) 31 | filterData.removeAt(0) 32 | 33 | filterData.forEach { 34 | fLick.add( 35 | it.joinToString( 36 | separator = "%22%2C%22", 37 | prefix = "%5B%22", 38 | postfix = "%22%5D" 39 | ) 40 | ) 41 | } 42 | 43 | return fLick.joinToString( 44 | separator = "%2C", 45 | prefix = "%5B", 46 | postfix = "%5D" 47 | ) 48 | } 49 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_settings_container.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 14 | 15 | 26 | 27 | 28 | 29 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/java/me/theclashfruit/rithle/fragments/SettingsContainerFragment.kt: -------------------------------------------------------------------------------- 1 | package me.theclashfruit.rithle.fragments 2 | 3 | import android.annotation.SuppressLint 4 | import android.os.Bundle 5 | import androidx.fragment.app.Fragment 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import android.widget.FrameLayout 10 | import com.google.android.material.appbar.MaterialToolbar 11 | import me.theclashfruit.rithle.R 12 | 13 | class SettingsContainerFragment : Fragment() { 14 | override fun onCreate(savedInstanceState: Bundle?) { 15 | super.onCreate(savedInstanceState) 16 | arguments?.let {} 17 | } 18 | 19 | override fun onCreateView( 20 | inflater: LayoutInflater, container: ViewGroup?, 21 | savedInstanceState: Bundle? 22 | ): View? { 23 | val rootView = inflater.inflate(R.layout.fragment_settings_container, container, false) 24 | 25 | val toolBar: MaterialToolbar = rootView.findViewById(R.id.toolbar) 26 | 27 | toolBar.setNavigationOnClickListener { 28 | parentFragmentManager.popBackStack() 29 | } 30 | 31 | val fragmentTransaction = parentFragmentManager.beginTransaction() 32 | val settingsFragment = SettingsFragment() 33 | 34 | fragmentTransaction 35 | .replace(R.id.settingsContainer, settingsFragment) 36 | .commit() 37 | 38 | return rootView 39 | } 40 | 41 | companion object { 42 | @JvmStatic 43 | fun newInstance() = 44 | SettingsContainerFragment().apply { 45 | arguments = Bundle().apply {} 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 17 | 18 | 19 | 20 | 21 | 23 | -------------------------------------------------------------------------------- /app/src/main/java/me/theclashfruit/rithle/fragments/ProjectDescriptionFragment.kt: -------------------------------------------------------------------------------- 1 | package me.theclashfruit.rithle.fragments 2 | 3 | import android.os.Bundle 4 | import androidx.fragment.app.Fragment 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import kotlinx.serialization.decodeFromString 9 | import kotlinx.serialization.json.Json 10 | import me.theclashfruit.rithle.R 11 | import me.theclashfruit.rithle.models.ModrinthProjectModel 12 | import me.theclashfruit.rithle.models.ModrinthSearchModel 13 | import org.json.JSONArray 14 | import org.json.JSONObject 15 | 16 | class ProjectDescriptionFragment : Fragment() { 17 | private var projectData: ModrinthProjectModel? = null 18 | 19 | override fun onCreate(savedInstanceState: Bundle?) { 20 | super.onCreate(savedInstanceState) 21 | 22 | val format = Json { ignoreUnknownKeys = true } 23 | 24 | arguments?.let { 25 | projectData = format.decodeFromString(it.getString("projectData")!!) 26 | } 27 | } 28 | 29 | override fun onCreateView( 30 | inflater: LayoutInflater, container: ViewGroup?, 31 | savedInstanceState: Bundle? 32 | ): View? { 33 | // Inflate the layout for this fragment 34 | return inflater.inflate(R.layout.fragment_project_description, container, false) 35 | } 36 | 37 | companion object { 38 | @JvmStatic 39 | fun newInstance(projectDataString: String) = 40 | ProjectDescriptionFragment().apply { 41 | arguments = Bundle().apply { 42 | putString("projectData", projectDataString) 43 | } 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_share.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_extension.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/java/me/theclashfruit/rithle/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package me.theclashfruit.rithle 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.net.Uri 6 | import androidx.appcompat.app.AppCompatActivity 7 | import android.os.Bundle 8 | import androidx.browser.customtabs.CustomTabsIntent 9 | import androidx.core.content.ContextCompat 10 | import com.google.android.material.dialog.MaterialAlertDialogBuilder 11 | import me.theclashfruit.rithle.fragments.HomeFragment 12 | import me.theclashfruit.rithle.services.NotificationService 13 | 14 | class MainActivity : AppCompatActivity() { 15 | 16 | override fun onCreate(savedInstanceState: Bundle?) { 17 | super.onCreate(savedInstanceState) 18 | setContentView(R.layout.activity_main) 19 | 20 | val mainFragmentTransaction = supportFragmentManager.beginTransaction() 21 | val homeFragment = HomeFragment.newInstance() 22 | 23 | val sharedPref = getSharedPreferences("me.theclashfruit.rithle_preferences", Context.MODE_PRIVATE) 24 | val authToken = sharedPref!!.getString("authToken", "") 25 | 26 | if(authToken != "") 27 | ContextCompat.startForegroundService(this, Intent(this, NotificationService::class.java)) 28 | 29 | mainFragmentTransaction 30 | .replace(R.id.parentFragmentContainer, homeFragment) 31 | .commit() 32 | 33 | /* 34 | MaterialAlertDialogBuilder(this) 35 | .setTitle("⚠️ Warning!") 36 | .setMessage("This is an alpha build, it is not intended for regular use, report bugs on GitHub.") 37 | .setPositiveButton("Ok") { dialog, which -> 38 | 39 | } 40 | .show() 41 | */ 42 | 43 | // https://github.com/login/oauth/authorize?client_id=2f7fbf1e6e196b0d2069 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/res/layout/drawer_header.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 27 | 28 | 43 | 44 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_settings.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 28 | 29 | 32 | 33 | 36 | 37 | 42 | -------------------------------------------------------------------------------- /app/src/main/res/layout/list_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 14 | 15 | 20 | 21 | 22 | 23 | 28 | 29 | 37 | 38 | 45 | 46 | 53 | 54 | -------------------------------------------------------------------------------- /app/src/main/java/me/theclashfruit/rithle/models/ModrinthSearchHitsModel.kt: -------------------------------------------------------------------------------- 1 | package me.theclashfruit.rithle.models 2 | 3 | import com.google.gson.annotations.SerializedName 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | data class ModrinthSearchHitsModel( 9 | @SerializedName("project_id" ) var project_id : String? = null, 10 | @SerializedName("project_type" ) var project_type : String? = null, 11 | @SerializedName("slug" ) var slug : String? = null, 12 | @SerializedName("author" ) var author : String? = null, 13 | @SerializedName("title" ) var title : String? = null, 14 | @SerializedName("description" ) var description : String? = null, 15 | @SerializedName("categories" ) var categories : ArrayList = arrayListOf(), 16 | @SerializedName("display_categories" ) var display_categories : ArrayList = arrayListOf(), 17 | @SerializedName("versions" ) var versions : ArrayList = arrayListOf(), 18 | @SerializedName("downloads" ) var downloads : Int? = null, 19 | @SerializedName("follows" ) var follows : Int? = null, 20 | @SerializedName("icon_url" ) var icon_url : String? = null, 21 | @SerializedName("date_created" ) var date_created : String? = null, 22 | @SerializedName("date_modified" ) var date_modified : String? = null, 23 | @SerializedName("latest_version" ) var latest_version : String? = null, 24 | @SerializedName("license" ) var license : String? = null, 25 | @SerializedName("client_side" ) var client_side : String? = null, 26 | @SerializedName("server_side" ) var server_side : String? = null, 27 | @SerializedName("gallery" ) var gallery : ArrayList = arrayListOf() 28 | ) -------------------------------------------------------------------------------- /app/src/main/res/layout/list_downloads.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 21 | 22 | 34 | 35 | 47 | -------------------------------------------------------------------------------- /app/src/main/res/xml/root_preferences.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 9 | 14 | 15 | 16 | 17 | 23 | 28 | 29 | 30 | 31 | 36 | 41 | 42 | 43 | 46 | 49 | 50 | -------------------------------------------------------------------------------- /app/src/main/java/me/theclashfruit/rithle/models/ModrinthProjectModel.kt: -------------------------------------------------------------------------------- 1 | package me.theclashfruit.rithle.models 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class ModrinthProjectModel ( 7 | var id : String? = null, 8 | var slug : String? = null, 9 | var project_type : String? = null, 10 | var team : String? = null, 11 | var title : String? = null, 12 | var description : String? = null, 13 | var body : String? = null, 14 | var body_url : String? = null, 15 | var published : String? = null, 16 | var updated : String? = null, 17 | var approved : String? = null, 18 | var status : String? = null, 19 | var moderator_message : ModrinthModMessageModel? = ModrinthModMessageModel(), 20 | var license : ModrinthLicenseModel? = ModrinthLicenseModel(), 21 | var client_side : String? = null, 22 | var server_side : String? = null, 23 | var downloads : Int? = null, 24 | var followers : Int? = null, 25 | var categories : ArrayList = arrayListOf(), 26 | var additional_categories : ArrayList = arrayListOf(), 27 | var versions : ArrayList = arrayListOf(), 28 | var icon_url : String? = null, 29 | var issues_url : String? = null, 30 | var source_url : String? = null, 31 | var wiki_url : String? = null, 32 | var discord_url : String? = null, 33 | var donation_urls : ArrayList = arrayListOf(), 34 | var gallery : ArrayList = arrayListOf() 35 | ) -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_home.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 12 | 13 | 23 | 24 | 25 | 26 | 30 | 31 | 39 | 40 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /app/src/main/res/raw/licenses.txt: -------------------------------------------------------------------------------- 1 | ========== Volley == 2 | 3 | Copyright 2022 Google 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | Repository: https://github.com/google/volley 18 | Full License: https://github.com/google/volley/blob/master/LICENSE 19 | 20 | ==================== 21 | 22 | ========= Markwon == 23 | 24 | Copyright 2019 Dimitry Ivanov (legal@noties.io) 25 | 26 | Licensed under the Apache License, Version 2.0 (the "License"); 27 | you may not use this file except in compliance with the License. 28 | You may obtain a copy of the License at 29 | 30 | http://www.apache.org/licenses/LICENSE-2.0 31 | 32 | Unless required by applicable law or agreed to in writing, software 33 | distributed under the License is distributed on an "AS IS" BASIS, 34 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 35 | See the License for the specific language governing permissions and 36 | limitations under the License. 37 | 38 | Repository: https://github.com/noties/Markwon 39 | Full License: https://github.com/noties/Markwon/blob/master/LICENSE 40 | 41 | ==================== 42 | 43 | ========== Rithle == 44 | 45 | Rithle, Android app for Modrinth written in Kotlin. 46 | Copyright (C) 2022 TheClashFruit 47 | 48 | This program is free software: you can redistribute it and/or modify 49 | it under the terms of the GNU General Public License as published by 50 | the Free Software Foundation, either version 3 of the License, or 51 | (at your option) any later version. 52 | 53 | This program is distributed in the hope that it will be useful, 54 | but WITHOUT ANY WARRANTY; without even the implied warranty of 55 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 56 | GNU General Public License for more details. 57 | 58 | You should have received a copy of the GNU General Public License 59 | along with this program. If not, see . 60 | 61 | Repository: https://github.com/TheClashFruit/Rithle 62 | Full License: https://github.com/TheClashFruit/Rithle/blob/master/LICENSE 63 | 64 | ==================== -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_project_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 14 | 15 | 26 | 27 | 28 | 29 | 33 | 34 | 42 | 43 | 44 | 45 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 24 | 28 | 29 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Rithle 3 | 4 | Login 5 | Logout 6 | Profile 7 | Search 8 | Mods 9 | Plugins 10 | Resource Packs 11 | Modpacks 12 | Info 13 | Description 14 | Changelog 15 | Downloads 16 | Settings 17 | Report 18 | 19 | Theme 20 | Advanced 21 | Experimental 22 | About 23 | 24 | Material You 25 | Dark Mode 26 | Force the app to use dark mode even if the device is in light mode. 27 | GitHub Token 28 | Your GitHub token used to authenticate requests to Modrinth. 29 | Debug Mode 30 | Adds additional info to the logs. 31 | Proxy Images 32 | Proxies images in project descriptions. 33 | Modpack Creator 34 | Create modpacks with a packwiz compatible gui based editor. 35 | Licenses 36 | Copyright 37 | Copyright © 2022-2023 TheClashFruit 38 | 39 | There was an unexpected error while trying to process the authentication with GitHub, please enter a GitHub token in the settings. 40 | There was an error while trying to complete the authentication with Modrinth, please enter a GitHub token in the settings:\n%s 41 | 42 | 43 | %s Download 44 | %s Downloads 45 | 46 | 47 | %s Follower 48 | %s Followers 49 | 50 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'org.jetbrains.kotlin.android' 4 | id 'org.jetbrains.kotlin.plugin.serialization' 5 | } 6 | 7 | android { 8 | compileSdk 33 9 | 10 | defaultConfig { 11 | applicationId "me.theclashfruit.rithle" 12 | minSdk 27 13 | targetSdk 33 14 | versionCode 3 15 | versionName "0.3.0-beta" 16 | 17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 18 | 19 | Properties properties = new Properties() 20 | properties.load(project.rootProject.file("local.properties").newDataInputStream()) 21 | 22 | buildConfigField "String", "GH_SECRET", "${properties.getProperty("ghsecret")}" 23 | buildConfigField "String", "GH_CLIENT", "${properties.getProperty("ghclient")}" 24 | } 25 | 26 | buildTypes { 27 | release { 28 | minifyEnabled false 29 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 30 | } 31 | } 32 | 33 | compileOptions { 34 | sourceCompatibility JavaVersion.VERSION_1_8 35 | targetCompatibility JavaVersion.VERSION_1_8 36 | } 37 | 38 | kotlinOptions { 39 | jvmTarget = '1.8' 40 | } 41 | } 42 | 43 | configurations { 44 | cleanedAnnotations 45 | implementation.exclude group: 'org.jetbrains' , module:'annotations' 46 | } 47 | 48 | dependencies { 49 | implementation 'androidx.core:core-ktx:1.9.0' 50 | implementation 'androidx.appcompat:appcompat:1.5.1' 51 | implementation 'androidx.lifecycle:lifecycle-viewmodel:2.5.1' 52 | implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1' 53 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4' 54 | implementation 'androidx.preference:preference:1.2.0' 55 | implementation 'androidx.browser:browser:1.4.0' 56 | 57 | implementation 'com.google.android.material:material:1.7.0' 58 | 59 | implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1' 60 | 61 | implementation 'io.noties.markwon:core:4.6.2' 62 | implementation 'io.noties.markwon:ext-latex:4.6.2' 63 | implementation 'io.noties.markwon:ext-strikethrough:4.6.2' 64 | implementation 'io.noties.markwon:ext-tables:4.6.2' 65 | implementation 'io.noties.markwon:ext-tasklist:4.6.2' 66 | implementation 'io.noties.markwon:html:4.6.2' 67 | implementation 'io.noties.markwon:image:4.6.2' 68 | implementation 'io.noties.markwon:linkify:4.6.2' 69 | implementation 'io.noties.markwon:syntax-highlight:4.6.2' 70 | 71 | implementation 'com.google.code.gson:gson:2.10' 72 | implementation 'com.android.volley:volley:1.2.1' 73 | 74 | testImplementation 'junit:junit:4.13.2' 75 | 76 | androidTestImplementation 'androidx.test.ext:junit:1.1.4' 77 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0' 78 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_auth.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 20 | 21 | 30 | 31 | 40 | 41 | 55 | 56 | 65 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_user.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 14 | 15 | 27 | 28 | 29 | 30 | 34 | 35 | 36 | 50 | 51 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /app/src/main/java/me/theclashfruit/rithle/fragments/ProjectDownloadsFragment.kt: -------------------------------------------------------------------------------- 1 | package me.theclashfruit.rithle.fragments 2 | 3 | import android.annotation.SuppressLint 4 | import android.os.Build 5 | import android.os.Bundle 6 | import android.util.Log 7 | import androidx.fragment.app.Fragment 8 | import android.view.LayoutInflater 9 | import android.view.View 10 | import android.view.ViewGroup 11 | import androidx.recyclerview.widget.LinearLayoutManager 12 | import androidx.recyclerview.widget.RecyclerView 13 | import com.android.volley.toolbox.JsonArrayRequest 14 | import com.android.volley.toolbox.JsonObjectRequest 15 | import kotlinx.serialization.decodeFromString 16 | import me.theclashfruit.rithle.BuildConfig 17 | import me.theclashfruit.rithle.R 18 | import me.theclashfruit.rithle.adapters.DownloadsAdapter 19 | import me.theclashfruit.rithle.classes.MrApiUrlUtil 20 | import me.theclashfruit.rithle.classes.RithleSingleton 21 | import me.theclashfruit.rithle.models.ModrinthProjectModel 22 | 23 | class ProjectDownloadsFragment : Fragment() { 24 | private var modId: String? = null 25 | 26 | override fun onCreate(savedInstanceState: Bundle?) { 27 | super.onCreate(savedInstanceState) 28 | arguments?.let { 29 | modId = it.getString("modId") 30 | } 31 | } 32 | 33 | @SuppressLint("MissingInflatedId") 34 | override fun onCreateView( 35 | inflater: LayoutInflater, container: ViewGroup?, 36 | savedInstanceState: Bundle? 37 | ): View? { 38 | val rootView = inflater.inflate(R.layout.fragment_project_downloads, container, false) 39 | 40 | val recyclerView = rootView.findViewById(R.id.recyclerView) 41 | 42 | val jsonObjectRequest = @SuppressLint("SetTextI18n") 43 | object : JsonArrayRequest( 44 | Method.GET, MrApiUrlUtil().getApiUrl() + "/v2/project/${modId}/version", null, 45 | { response -> 46 | val listAdapter = DownloadsAdapter(response, requireContext(), parentFragmentManager) 47 | val layoutManager = LinearLayoutManager(requireContext()) 48 | 49 | recyclerView.layoutManager = layoutManager 50 | recyclerView.adapter = listAdapter 51 | }, 52 | { error -> 53 | Log.e("webCall", error.toString()) 54 | } 55 | ) { 56 | override fun getHeaders(): MutableMap { 57 | val headers = HashMap() 58 | headers["User-Agent"] = "Mozilla/5.0 (Linux; Android ${Build.VERSION.RELEASE}) Rithle/${BuildConfig.VERSION_NAME} (github.com/TheClashFruit/Rithle; admin@theclashfruit.me) Volley/1.2.1" 59 | return headers 60 | } 61 | } 62 | 63 | RithleSingleton.getInstance(requireContext()).addToRequestQueue(jsonObjectRequest) 64 | 65 | 66 | return rootView 67 | } 68 | 69 | companion object { 70 | @JvmStatic 71 | fun newInstance(modId: String?) = 72 | ProjectDownloadsFragment().apply { 73 | arguments = Bundle().apply { 74 | putString("modId", modId) 75 | } 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!IMPORTANT] 2 | > Please check out issue [#9](https://github.com/TheClashFruit/Rithle/issues/9) of you are wondering why there hasn't been an update since Febuary of 2023! 3 | 4 | ![ritle_banner](https://user-images.githubusercontent.com/55049569/218251089-1782245b-d742-4ecd-b743-77c18811dba6.svg) 5 | 6 |

7 | Rithle 8 |

9 | 10 |

11 | GitHub Workflow Status 12 | GitHub issues 13 | GitHub pull requests 14 | GitHub all releases 15 | 16 | GitHub 17 | GitHub commit activity 18 |

19 | 20 |

21 | Android app for Modrinth written in Kotlin. 22 |

23 | 24 |

25 | Screenshots 26 |

27 | 28 | ![screenshots_of_rithle](https://user-images.githubusercontent.com/55049569/222969306-a26a90d4-769a-4564-bd85-a4fa90638997.png) 29 | 30 |

31 | Building 32 |

33 | 34 | 40 | 41 |

42 |

    43 |
  1. Clone the repo with git clone https://github.com/TheClashFruit/Rithle.git
  2. 44 |
  3. Go into the folder with cd Rithle
  4. 45 |
  5. Add your github oauth secret and client id to local.properties as ghclient and ghsecret
  6. 46 |
  7. Build the app with ./gradlew build
  8. 47 |
48 |

49 | 50 |

51 | Contributing 52 |

53 | 54 |

55 | Can be found in CONTRIBUTING.md. 56 |

57 | 58 |

59 | Thanks To 60 |

61 | 62 |

63 |

    64 |
  • Modrinth - An awesome platform for sharing minecraft mods, plugins and more.
  • 65 |
  • Volley - Network requests.
  • 66 |
  • Markwon - Markdown rendering for the project descriptions.
  • 67 |
68 |

69 | 70 |

71 | License 72 |

73 | 74 |
75 |   Rithle, Android app for Modrinth written in Kotlin.
76 |   Copyright (C) 2022-2023 TheClashFruit
77 | 
78 |   This program is free software: you can redistribute it and/or modify
79 |   it under the terms of the GNU General Public License as published by
80 |   the Free Software Foundation, either version 3 of the License, or
81 |   (at your option) any later version.
82 | 
83 |   This program is distributed in the hope that it will be useful,
84 |   but WITHOUT ANY WARRANTY; without even the implied warranty of
85 |   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
86 |   GNU General Public License for more details.
87 | 
88 |   You should have received a copy of the GNU General Public License
89 |   along with this program.  If not, see <https://www.gnu.org/licenses/>.
90 | 
91 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 29 | 30 | 47 | 48 | 51 | 52 | 57 | 58 | 61 | 62 | 67 | 68 | 71 | -------------------------------------------------------------------------------- /app/src/main/java/me/theclashfruit/rithle/adapters/ModListAdapter.kt: -------------------------------------------------------------------------------- 1 | package me.theclashfruit.rithle.adapters 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.util.Log 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import android.widget.ImageView 10 | import android.widget.TextView 11 | import androidx.fragment.app.FragmentManager 12 | import androidx.recyclerview.widget.RecyclerView 13 | import com.android.volley.VolleyError 14 | import com.android.volley.toolbox.ImageLoader 15 | import me.theclashfruit.rithle.R 16 | import me.theclashfruit.rithle.classes.RithleSingleton 17 | import me.theclashfruit.rithle.fragments.ProjectViewFragment 18 | import me.theclashfruit.rithle.models.ModrinthSearchHitsModel 19 | 20 | class ModListAdapter(private val modList: ArrayList, private val appContext: Context, private val fragmentManager: FragmentManager) : RecyclerView.Adapter() { 21 | 22 | class StreamHolder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener { 23 | init { 24 | itemView.setOnClickListener(this) 25 | } 26 | 27 | override fun onClick(itemView: View) { 28 | Log.d("RecyclerView", "CLICK!") 29 | } 30 | } 31 | 32 | override fun onCreateViewHolder( 33 | parent: ViewGroup, 34 | viewType: Int 35 | ): StreamHolder { 36 | val layoutInflater = LayoutInflater.from(parent.context) 37 | 38 | val view: View = layoutInflater.inflate(R.layout.list_item, parent, false) 39 | 40 | return StreamHolder(view) 41 | } 42 | 43 | @SuppressLint("SetTextI18n") 44 | override fun onBindViewHolder(holder: StreamHolder, position: Int) { 45 | holder.itemView.findViewById(R.id.textView).text = modList[position].title 46 | holder.itemView.findViewById(R.id.textView2).text = "by ${modList[position].author}" 47 | holder.itemView.findViewById(R.id.textView3).text = modList[position].description 48 | 49 | if(modList[position].icon_url!!.isNotEmpty()) { 50 | RithleSingleton.getInstance(appContext).imageLoader.get( 51 | modList[position].icon_url.toString(), 52 | object : ImageLoader.ImageListener { 53 | override fun onResponse( 54 | response: ImageLoader.ImageContainer?, 55 | isImmediate: Boolean 56 | ) { 57 | if (response != null) { 58 | holder.itemView.findViewById(R.id.imageView) 59 | .setImageBitmap(response.bitmap) 60 | } 61 | } 62 | 63 | override fun onErrorResponse(error: VolleyError?) { 64 | Log.e("imageLoader", error!!.stackTraceToString()) 65 | } 66 | }) 67 | } 68 | 69 | holder.itemView.rootView.setOnClickListener { 70 | val mainFragmentTransaction = fragmentManager.beginTransaction() 71 | val projectViewFragment = ProjectViewFragment.newInstance(modList[position].project_id.toString()) 72 | 73 | mainFragmentTransaction 74 | .addToBackStack("projectViewFragment") 75 | .add(R.id.parentFragmentContainer, projectViewFragment) 76 | .commit() 77 | } 78 | } 79 | 80 | override fun getItemCount() = modList.size 81 | } -------------------------------------------------------------------------------- /app/src/main/java/me/theclashfruit/rithle/adapters/DownloadsAdapter.kt: -------------------------------------------------------------------------------- 1 | package me.theclashfruit.rithle.adapters 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.net.Uri 6 | import android.util.Log 7 | import android.view.LayoutInflater 8 | import android.view.View 9 | import android.view.ViewGroup 10 | import android.widget.ImageView 11 | import android.widget.TextView 12 | import androidx.browser.customtabs.CustomTabColorSchemeParams 13 | import androidx.browser.customtabs.CustomTabsIntent 14 | import androidx.core.content.ContentProviderCompat.requireContext 15 | import androidx.core.content.ContextCompat 16 | import androidx.fragment.app.FragmentManager 17 | import androidx.recyclerview.widget.RecyclerView 18 | import kotlinx.serialization.json.Json 19 | import me.theclashfruit.rithle.R 20 | import me.theclashfruit.rithle.models.ModrinthSearchHitsModel 21 | import org.json.JSONArray 22 | import org.json.JSONObject 23 | 24 | class DownloadsAdapter(private val downloadsList: JSONArray, private val appContext: Context, private val fragmentManager: FragmentManager) : RecyclerView.Adapter() { 25 | 26 | class StreamHolder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener { 27 | init { 28 | itemView.setOnClickListener(this) 29 | } 30 | 31 | override fun onClick(itemView: View) { 32 | Log.d("RecyclerView", "CLICK!") 33 | } 34 | } 35 | 36 | override fun onCreateViewHolder( 37 | parent: ViewGroup, 38 | viewType: Int 39 | ): StreamHolder { 40 | val layoutInflater = LayoutInflater.from(parent.context) 41 | 42 | val view: View = layoutInflater.inflate(R.layout.list_downloads, parent, false) 43 | 44 | return StreamHolder(view) 45 | } 46 | 47 | @SuppressLint("SetTextI18n") 48 | override fun onBindViewHolder(holder: StreamHolder, position: Int) { 49 | val versionNumber: TextView = holder.itemView.findViewById(R.id.textView9) 50 | val versionCompatibility: TextView = holder.itemView.findViewById(R.id.textView10) 51 | 52 | val downloadButton: ImageView = holder.itemView.findViewById(R.id.imageView3) 53 | 54 | val data = downloadsList.getJSONObject(position) 55 | var versionList = "" 56 | 57 | for (i in 0 until data.getJSONArray("game_versions").length()) { 58 | versionList = if (i == 0) 59 | data.getJSONArray("game_versions").getString(i) 60 | else 61 | versionList + ", " + data.getJSONArray("game_versions").getString(i) 62 | } 63 | 64 | versionNumber.text = data.getString("version_number") 65 | versionCompatibility.text = versionList 66 | 67 | downloadButton.setOnClickListener { 68 | CustomTabsIntent.Builder() 69 | .setColorScheme(CustomTabsIntent.COLOR_SCHEME_SYSTEM) 70 | .setColorSchemeParams( 71 | CustomTabsIntent.COLOR_SCHEME_DARK, 72 | CustomTabColorSchemeParams.Builder() 73 | .setToolbarColor( 74 | ContextCompat.getColor( 75 | appContext, 76 | R.color.colorPrimary 77 | ) 78 | ) 79 | .build() 80 | ) 81 | .setDefaultColorSchemeParams( 82 | CustomTabColorSchemeParams.Builder() 83 | .setToolbarColor( 84 | ContextCompat.getColor( 85 | appContext, 86 | R.color.colorPrimaryLight 87 | ) 88 | ) 89 | .build() 90 | ) 91 | .build() 92 | .launchUrl(appContext, Uri.parse(data.getJSONArray("files").getJSONObject(0).getString("url"))) 93 | } 94 | } 95 | 96 | override fun getItemCount() = downloadsList.length() 97 | } -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 119 | 120 | 125 | 126 | -------------------------------------------------------------------------------- /app/src/main/java/me/theclashfruit/rithle/services/NotificationService.kt: -------------------------------------------------------------------------------- 1 | package me.theclashfruit.rithle.services 2 | 3 | import android.app.* 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.os.Build 7 | import android.os.Handler 8 | import android.os.IBinder 9 | import android.util.Log 10 | import androidx.core.app.NotificationCompat 11 | import androidx.core.app.NotificationManagerCompat 12 | import com.android.volley.toolbox.JsonArrayRequest 13 | import me.theclashfruit.rithle.BuildConfig 14 | import me.theclashfruit.rithle.R 15 | import me.theclashfruit.rithle.classes.MrApiUrlUtil 16 | import me.theclashfruit.rithle.classes.RithleSingleton 17 | import java.time.Instant 18 | import java.time.format.DateTimeFormatter 19 | import java.time.temporal.TemporalAccessor 20 | import java.util.* 21 | 22 | 23 | class NotificationService : Service() { 24 | private val theHandler = Handler(); 25 | 26 | override fun onCreate() { 27 | val checkNotifs: Runnable = object : Runnable { 28 | override fun run() { 29 | val sharedPref = applicationContext.getSharedPreferences("me.theclashfruit.rithle_preferences", Context.MODE_PRIVATE) 30 | 31 | val authToken = sharedPref!!.getString("authToken", "") 32 | val userName = sharedPref.getString("userName", "@me") 33 | 34 | if(MrApiUrlUtil().getIsDebugMode()) 35 | return 36 | 37 | val jsonObjectRequest = object : JsonArrayRequest( 38 | Method.GET, "https://api.modrinth.com/v2/user/${userName}/notifications", null, 39 | { response -> 40 | for (i in 0 until response.length()) { 41 | Log.d("shitShit", response.getJSONObject(i).toString()) 42 | 43 | val cNotif = response.getJSONObject(i) 44 | 45 | val notification: Notification = NotificationCompat.Builder(applicationContext, "rthl") 46 | .setContentTitle(cNotif.getString("title")) 47 | .setContentText(cNotif.getString("text")) 48 | .setSmallIcon(R.drawable.ic_update) 49 | .setColor(0x1bd96b) 50 | .setPriority(NotificationCompat.PRIORITY_DEFAULT) 51 | .build() 52 | 53 | val timeFormatter: DateTimeFormatter = DateTimeFormatter.ISO_DATE_TIME 54 | val accessor: TemporalAccessor = timeFormatter.parse(cNotif.getString("created")) 55 | 56 | val date = Date.from(Instant.from(accessor)) 57 | 58 | with(NotificationManagerCompat.from(applicationContext)) { 59 | notify(date.time.toInt(), notification) 60 | } 61 | } 62 | }, 63 | { error -> 64 | Log.e("webCall", error.stackTraceToString()) 65 | } 66 | ) { 67 | override fun getHeaders(): MutableMap { 68 | val headers = HashMap() 69 | headers["User-Agent"] = "Mozilla/5.0 (Linux; Android ${Build.VERSION.RELEASE}) Rithle/${BuildConfig.VERSION_NAME} (github.com/TheClashFruit/Rithle; admin@theclashfruit.me) Volley/1.2.1" 70 | headers["Authorization"] = authToken.toString() 71 | return headers 72 | } 73 | } 74 | 75 | RithleSingleton.getInstance(applicationContext).addToRequestQueue(jsonObjectRequest) 76 | } 77 | } 78 | 79 | checkNotifs.run() 80 | 81 | val channel = NotificationChannel("rthl", "Rithle Notification Service", NotificationManager.IMPORTANCE_DEFAULT) 82 | val notificationManager: NotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager 83 | 84 | notificationManager.createNotificationChannel(channel) 85 | 86 | super.onCreate() 87 | } 88 | 89 | override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { 90 | val notification: Notification = NotificationCompat.Builder(this, "rthl") 91 | .setContentTitle("Subscribed to the Notifications") 92 | .setContentText("Service is running...") 93 | .setSmallIcon(R.drawable.ic_info) 94 | .setColor(0x1bd96b) 95 | .build() 96 | 97 | startForeground(1, notification) 98 | 99 | with(NotificationManagerCompat.from(this)) { 100 | cancel(1) 101 | } 102 | 103 | return START_STICKY 104 | } 105 | 106 | override fun onBind(intent: Intent): IBinder? { 107 | return null 108 | } 109 | } -------------------------------------------------------------------------------- /app/src/main/java/me/theclashfruit/rithle/fragments/ListFragment.kt: -------------------------------------------------------------------------------- 1 | package me.theclashfruit.rithle.fragments 2 | 3 | import android.content.Context 4 | import android.os.Build 5 | import android.os.Bundle 6 | import android.util.Log 7 | import android.view.LayoutInflater 8 | import android.view.View 9 | import android.view.ViewGroup 10 | import androidx.core.widget.NestedScrollView 11 | import androidx.fragment.app.Fragment 12 | import androidx.recyclerview.widget.DiffUtil 13 | import androidx.recyclerview.widget.LinearLayoutManager 14 | import androidx.recyclerview.widget.RecyclerView 15 | import com.android.volley.toolbox.JsonObjectRequest 16 | import kotlinx.serialization.decodeFromString 17 | import kotlinx.serialization.json.Json 18 | import me.theclashfruit.rithle.BuildConfig 19 | import me.theclashfruit.rithle.R 20 | import me.theclashfruit.rithle.adapters.ModListAdapter 21 | import me.theclashfruit.rithle.classes.ListDiffCallback 22 | import me.theclashfruit.rithle.classes.MrApiUrlUtil 23 | import me.theclashfruit.rithle.classes.RithleSingleton 24 | import me.theclashfruit.rithle.models.ModrinthSearchHitsModel 25 | import me.theclashfruit.rithle.models.ModrinthSearchModel 26 | 27 | class ListFragment : Fragment() { 28 | private var currentIndex = 10 29 | private var lastIndex = 0 30 | 31 | private var allData: ArrayList = arrayListOf() 32 | 33 | private var listAdapter: ModListAdapter? = null 34 | private var layoutManager: LinearLayoutManager? = null 35 | 36 | private var recyclerView: RecyclerView? = null 37 | private var nestedScrollView: NestedScrollView? = null 38 | 39 | private var filter: String? = null 40 | 41 | override fun onCreate(savedInstanceState: Bundle?) { 42 | super.onCreate(savedInstanceState) 43 | arguments?.let { 44 | filter = it.getString("projectFilters") 45 | } 46 | } 47 | 48 | override fun onCreateView( 49 | inflater: LayoutInflater, container: ViewGroup?, 50 | savedInstanceState: Bundle? 51 | ): View? { 52 | val rootView = inflater.inflate(R.layout.fragment_list, container, false) 53 | 54 | recyclerView = rootView.findViewById(R.id.recyclerView) 55 | nestedScrollView = rootView.findViewById(R.id.nestedScrollView) 56 | 57 | listAdapter = ModListAdapter(allData, requireContext(), parentFragmentManager) 58 | layoutManager = LinearLayoutManager(requireContext()) 59 | 60 | recyclerView!!.layoutManager = layoutManager 61 | recyclerView!!.adapter = listAdapter 62 | 63 | getItems(requireContext()) 64 | 65 | nestedScrollView!!.setOnScrollChangeListener(NestedScrollView.OnScrollChangeListener { v, scrollX, scrollY, oldScrollX, oldScrollY -> 66 | if (scrollY == v.getChildAt(0).measuredHeight - v.measuredHeight) 67 | getItems(requireContext()) 68 | }) 69 | 70 | return rootView 71 | } 72 | 73 | companion object { 74 | @JvmStatic 75 | fun newInstance(projectFilters: String) = 76 | ListFragment().apply { 77 | arguments = Bundle().apply { 78 | putString("projectFilters", projectFilters) 79 | } 80 | } 81 | } 82 | 83 | private fun getItems(context: Context) { 84 | val format = Json { ignoreUnknownKeys = true } 85 | 86 | Log.d("YesFilter", MrApiUrlUtil().getApiUrl() + "/v2/search?limit=${currentIndex}&offset=${lastIndex}&index=relevance&facets=${filter}") 87 | 88 | val jsonObjectRequest = object : JsonObjectRequest( 89 | Method.GET, MrApiUrlUtil().getApiUrl() + "/v2/search?limit=${currentIndex}&offset=${lastIndex}&index=relevance&facets=${filter}", null, 90 | { response -> 91 | currentIndex = 10 92 | lastIndex += 10 93 | 94 | val data = format.decodeFromString(response.toString()) 95 | 96 | for (hit in data.hits) { 97 | allData.add(hit) 98 | 99 | val oldAllData = allData 100 | 101 | DiffUtil.calculateDiff(ListDiffCallback(oldAllData, allData)) 102 | .dispatchUpdatesTo(listAdapter!!) 103 | 104 | recyclerView!!.adapter = listAdapter 105 | } 106 | }, 107 | { error -> 108 | Log.e("webCall", error.toString()) 109 | } 110 | ) { 111 | override fun getHeaders(): MutableMap { 112 | val headers = HashMap() 113 | headers["User-Agent"] = "Mozilla/5.0 (Linux; Android ${Build.VERSION.RELEASE}) Rithle/${BuildConfig.VERSION_NAME} (github.com/TheClashFruit/Rithle; admin@theclashfruit.me) Volley/1.2.1" 114 | return headers 115 | } 116 | } 117 | 118 | RithleSingleton.getInstance(context).addToRequestQueue(jsonObjectRequest) 119 | } 120 | } -------------------------------------------------------------------------------- /app/src/main/java/me/theclashfruit/rithle/fragments/ProjectViewFragment.kt: -------------------------------------------------------------------------------- 1 | package me.theclashfruit.rithle.fragments 2 | 3 | import android.annotation.SuppressLint 4 | import android.os.Build 5 | import android.os.Bundle 6 | import android.util.Log 7 | import android.view.LayoutInflater 8 | import android.view.View 9 | import android.view.ViewGroup 10 | import androidx.fragment.app.Fragment 11 | import com.android.volley.toolbox.JsonObjectRequest 12 | import com.google.android.material.appbar.MaterialToolbar 13 | import com.google.android.material.bottomnavigation.BottomNavigationView 14 | import kotlinx.serialization.decodeFromString 15 | import kotlinx.serialization.json.Json 16 | import me.theclashfruit.rithle.BuildConfig 17 | import me.theclashfruit.rithle.R 18 | import me.theclashfruit.rithle.classes.MrApiUrlUtil 19 | import me.theclashfruit.rithle.classes.RithleSingleton 20 | import me.theclashfruit.rithle.models.ModrinthProjectModel 21 | 22 | class ProjectViewFragment : Fragment() { 23 | private var modId: String? = null 24 | private var dataRaw: String? = null 25 | 26 | override fun onCreate(savedInstanceState: Bundle?) { 27 | super.onCreate(savedInstanceState) 28 | arguments?.let { 29 | modId = it.getString("modId") 30 | } 31 | } 32 | 33 | @SuppressLint("MissingInflatedId") 34 | override fun onCreateView( 35 | inflater: LayoutInflater, container: ViewGroup?, 36 | savedInstanceState: Bundle? 37 | ): View? { 38 | val rootView = inflater.inflate(R.layout.fragment_project_view, container, false) 39 | 40 | val toolBar: MaterialToolbar = rootView.findViewById(R.id.toolbar) 41 | val bottomNavBar: BottomNavigationView = rootView.findViewById(R.id.bottomNavigation) 42 | 43 | toolBar.setNavigationOnClickListener { 44 | parentFragmentManager.popBackStack() 45 | } 46 | 47 | bottomNavBar.setOnNavigationItemSelectedListener { item -> 48 | val bottomNavFragmentTransaction = parentFragmentManager.beginTransaction() 49 | 50 | when(item.itemId) { 51 | R.id.itemInfo -> { 52 | val infoFragment = ProjectInfoFragment.newInstance(dataRaw!!) 53 | 54 | bottomNavFragmentTransaction 55 | .replace(R.id.projectFragmentContainer, infoFragment) 56 | .commit() 57 | 58 | return@setOnNavigationItemSelectedListener true 59 | } 60 | R.id.itemChangelog -> { 61 | val descriptionFragment = ProjectDescriptionFragment.newInstance(dataRaw!!) 62 | 63 | bottomNavFragmentTransaction 64 | .replace(R.id.projectFragmentContainer, descriptionFragment) 65 | .commit() 66 | 67 | return@setOnNavigationItemSelectedListener true 68 | } 69 | R.id.itemDownloads -> { 70 | val downloadsFragment = ProjectDownloadsFragment.newInstance(modId) 71 | 72 | bottomNavFragmentTransaction 73 | .replace(R.id.projectFragmentContainer, downloadsFragment) 74 | .commit() 75 | 76 | return@setOnNavigationItemSelectedListener true 77 | } 78 | R.id.itemSettings -> { 79 | 80 | return@setOnNavigationItemSelectedListener true 81 | } 82 | else -> false 83 | } 84 | } 85 | 86 | val format = Json { ignoreUnknownKeys = true } 87 | 88 | val jsonObjectRequest = @SuppressLint("SetTextI18n") 89 | object : JsonObjectRequest( 90 | Method.GET, MrApiUrlUtil().getApiUrl() + "/v2/project/${modId}", null, 91 | { response -> 92 | dataRaw = response.toString() 93 | val dataJson = format.decodeFromString(response.toString()) 94 | 95 | toolBar.subtitle = dataJson.title 96 | 97 | val infoFragment = ProjectInfoFragment.newInstance(dataRaw!!) 98 | 99 | parentFragmentManager.beginTransaction() 100 | .replace(R.id.projectFragmentContainer, infoFragment) 101 | .commit() 102 | }, 103 | { error -> 104 | Log.e("webCall", error.toString()) 105 | } 106 | ) { 107 | override fun getHeaders(): MutableMap { 108 | val headers = HashMap() 109 | headers["User-Agent"] = "Mozilla/5.0 (Linux; Android ${Build.VERSION.RELEASE}) Rithle/${BuildConfig.VERSION_NAME} (github.com/TheClashFruit/Rithle; admin@theclashfruit.me) Volley/1.2.1" 110 | return headers 111 | } 112 | } 113 | 114 | RithleSingleton.getInstance(requireContext()).addToRequestQueue(jsonObjectRequest) 115 | 116 | return rootView 117 | } 118 | 119 | companion object { 120 | @JvmStatic 121 | fun newInstance(modId: String) = 122 | ProjectViewFragment().apply { 123 | arguments = Bundle().apply { 124 | putString("modId", modId) 125 | } 126 | } 127 | } 128 | } -------------------------------------------------------------------------------- /app/src/main/java/me/theclashfruit/rithle/AuthActivity.kt: -------------------------------------------------------------------------------- 1 | package me.theclashfruit.rithle 2 | 3 | import android.content.Context 4 | import android.net.Uri 5 | import android.os.Build 6 | import androidx.appcompat.app.AppCompatActivity 7 | import android.os.Bundle 8 | import android.util.Log 9 | import android.widget.TextView 10 | import androidx.recyclerview.widget.DiffUtil 11 | import com.android.volley.Request 12 | import com.android.volley.toolbox.JsonObjectRequest 13 | import com.android.volley.toolbox.StringRequest 14 | import kotlinx.serialization.decodeFromString 15 | import kotlinx.serialization.json.Json 16 | import me.theclashfruit.rithle.classes.ListDiffCallback 17 | import me.theclashfruit.rithle.classes.MrApiUrlUtil 18 | import me.theclashfruit.rithle.classes.RithleSingleton 19 | import me.theclashfruit.rithle.models.GitHubAccessTokenModel 20 | import me.theclashfruit.rithle.models.ModrinthSearchModel 21 | 22 | class AuthActivity : AppCompatActivity() { 23 | private var progressTextView: TextView? = null 24 | 25 | override fun onCreate(savedInstanceState: Bundle?) { 26 | super.onCreate(savedInstanceState) 27 | setContentView(R.layout.activity_auth) 28 | 29 | val action: String? = intent?.action 30 | val data: Uri? = intent?.data 31 | 32 | progressTextView = findViewById(R.id.progressTextView) 33 | 34 | val sharedPref = getSharedPreferences("me.theclashfruit.rithle_preferences", Context.MODE_PRIVATE) 35 | 36 | val format = Json { ignoreUnknownKeys = true } 37 | 38 | // https://api.modrinth.com/v2/user 39 | 40 | val jsonObjectRequest = object : StringRequest( 41 | Method.POST, "https://github.com/login/oauth/access_token", 42 | { response -> 43 | val responseJson = format.decodeFromString(response) 44 | 45 | sharedPref.edit() 46 | .putString("authToken", responseJson.access_token!!) 47 | .apply() 48 | 49 | getMrUser(responseJson.access_token!!) 50 | 51 | progressTextView!!.text = "Requesting user from Modrinth..." 52 | 53 | Log.d("AuthUri", responseJson.access_token!!) 54 | }, 55 | { error -> 56 | progressTextView!!.text = resources.getString(R.string.auth_error_ghstep) 57 | 58 | error.networkResponse.allHeaders?.forEach { 59 | Log.e("webCall", it.name + ": " + it.value) 60 | } 61 | 62 | Log.e("webCall", "Code: " + error.networkResponse.statusCode.toString()) 63 | Log.e("webCall", "Data: " + error.networkResponse.data.decodeToString()) 64 | Log.e("webCall", "Time: " + error.networkResponse.networkTimeMs.toString()) 65 | } 66 | ) { 67 | override fun getParams(): Map { 68 | val params: MutableMap = HashMap() 69 | params["client_id"] = BuildConfig.GH_CLIENT 70 | params["client_secret"] = BuildConfig.GH_SECRET 71 | params["code"] = data!!.getQueryParameter("code")!!.trim() 72 | return params 73 | } 74 | 75 | override fun getHeaders(): MutableMap { 76 | val headers = HashMap() 77 | headers["User-Agent"] = "Mozilla/5.0 (Linux; Android ${Build.VERSION.RELEASE}) Rithle/${BuildConfig.VERSION_NAME} (github.com/TheClashFruit/Rithle; admin@theclashfruit.me) Volley/1.2.1" 78 | headers["Accept"] = "application/json" 79 | headers["Content-Type"] = "application/x-www-form-urlencoded; charset=utf-8" 80 | return headers 81 | } 82 | } 83 | 84 | RithleSingleton.getInstance(this).addToRequestQueue(jsonObjectRequest) 85 | } 86 | 87 | fun getMrUser(accessToken: String) { 88 | val sharedPref = getSharedPreferences("me.theclashfruit.rithle_preferences", Context.MODE_PRIVATE) 89 | 90 | val jsonObjectRequest = object : JsonObjectRequest( 91 | Method.GET, MrApiUrlUtil().getApiUrl() + "/v2/user", null, 92 | { response -> 93 | progressTextView!!.text = "Welcome ${response.getString("username")}!" 94 | 95 | sharedPref.edit() 96 | .putString("userName", response.getString("username")) 97 | .apply() 98 | 99 | finish() 100 | }, 101 | { error -> 102 | progressTextView!!.text = resources.getString(R.string.auth_error_mrstep, accessToken) 103 | Log.e("webCall", error.stackTraceToString()) 104 | } 105 | ) { 106 | override fun getHeaders(): MutableMap { 107 | val headers = HashMap() 108 | headers["User-Agent"] = "Mozilla/5.0 (Linux; Android ${Build.VERSION.RELEASE}) Rithle/${BuildConfig.VERSION_NAME} (github.com/TheClashFruit/Rithle; admin@theclashfruit.me) Volley/1.2.1" 109 | headers["Authorization"] = accessToken 110 | return headers 111 | } 112 | } 113 | 114 | RithleSingleton.getInstance(this).addToRequestQueue(jsonObjectRequest) 115 | } 116 | } -------------------------------------------------------------------------------- /app/src/main/java/me/theclashfruit/rithle/fragments/UserFragment.kt: -------------------------------------------------------------------------------- 1 | package me.theclashfruit.rithle.fragments 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.net.Uri 6 | import android.os.Build 7 | import android.os.Bundle 8 | import android.util.Log 9 | import android.view.LayoutInflater 10 | import android.view.View 11 | import android.view.ViewGroup 12 | import android.widget.TextView 13 | import androidx.browser.customtabs.CustomTabColorSchemeParams 14 | import androidx.browser.customtabs.CustomTabsIntent 15 | import androidx.core.content.ContextCompat 16 | import androidx.fragment.app.Fragment 17 | import com.android.volley.toolbox.JsonObjectRequest 18 | import com.google.android.material.appbar.MaterialToolbar 19 | import me.theclashfruit.rithle.BuildConfig 20 | import me.theclashfruit.rithle.R 21 | import me.theclashfruit.rithle.classes.MrApiUrlUtil 22 | import me.theclashfruit.rithle.classes.RithleSingleton 23 | import java.text.NumberFormat 24 | import java.util.* 25 | import kotlin.collections.HashMap 26 | 27 | class UserFragment : Fragment() { 28 | private var userId: String? = null 29 | 30 | override fun onCreate(savedInstanceState: Bundle?) { 31 | super.onCreate(savedInstanceState) 32 | arguments?.let { 33 | 34 | } 35 | } 36 | 37 | override fun onCreateView( 38 | inflater: LayoutInflater, container: ViewGroup?, 39 | savedInstanceState: Bundle? 40 | ): View? { 41 | val rootView = inflater.inflate(R.layout.fragment_user, container, false) 42 | 43 | val sharedPref = activity?.getSharedPreferences("me.theclashfruit.rithle_preferences", Context.MODE_PRIVATE) 44 | 45 | val authToken = sharedPref!!.getString("authToken", "") 46 | 47 | if(authToken == "") { 48 | CustomTabsIntent.Builder() 49 | .setColorScheme(CustomTabsIntent.COLOR_SCHEME_SYSTEM) 50 | .setColorSchemeParams( 51 | CustomTabsIntent.COLOR_SCHEME_DARK, 52 | CustomTabColorSchemeParams.Builder() 53 | .setToolbarColor(ContextCompat.getColor(requireContext(), R.color.colorPrimary)) 54 | .build() 55 | ) 56 | .setDefaultColorSchemeParams( 57 | CustomTabColorSchemeParams.Builder() 58 | .setToolbarColor(ContextCompat.getColor(requireContext(), R.color.colorPrimaryLight)) 59 | .build() 60 | ) 61 | .build() 62 | .launchUrl(requireContext(), Uri.parse("https://github.com/login/oauth/authorize?client_id=2f7fbf1e6e196b0d2069")) 63 | 64 | parentFragmentManager.popBackStack() 65 | } 66 | 67 | val jsonObjectRequest = object : JsonObjectRequest( 68 | Method.GET, MrApiUrlUtil().getApiUrl() + "/v2/user", null, 69 | { response -> 70 | val format: NumberFormat = NumberFormat.getCurrencyInstance() 71 | format.maximumFractionDigits = 0 72 | format.currency = Currency.getInstance("USD") 73 | format.maximumFractionDigits = 2 74 | 75 | rootView.findViewById(R.id.textView7).text = "${response.getString("username")} [${response.getString("role").capitalize()}]" 76 | rootView.findViewById(R.id.textView8).text = "${format.format(response.getJSONObject("payout_data").getString("balance").toFloat())} earned so far.." 77 | 78 | userId = response.getString("username") 79 | 80 | Log.e("webCall", response.toString()) 81 | }, 82 | { error -> 83 | Log.e("webCall", error.stackTraceToString()) 84 | } 85 | ) { 86 | override fun getHeaders(): MutableMap { 87 | val headers = HashMap() 88 | headers["User-Agent"] = "Mozilla/5.0 (Linux; Android ${Build.VERSION.RELEASE}) Rithle/${BuildConfig.VERSION_NAME} (github.com/TheClashFruit/Rithle; admin@theclashfruit.me) Volley/1.2.1" 89 | headers["Authorization"] = authToken.toString() 90 | return headers 91 | } 92 | } 93 | 94 | RithleSingleton.getInstance(requireContext()).addToRequestQueue(jsonObjectRequest) 95 | 96 | val toolBar: MaterialToolbar = rootView.findViewById(R.id.toolbar) 97 | 98 | toolBar.setNavigationOnClickListener { 99 | parentFragmentManager.popBackStack() 100 | } 101 | 102 | toolBar.setOnMenuItemClickListener { item -> 103 | when(item.itemId) { 104 | R.id.toolbarEdit -> { 105 | return@setOnMenuItemClickListener true 106 | } 107 | R.id.toolbarShare -> { 108 | val sendIntent: Intent = Intent().apply { 109 | action = Intent.ACTION_SEND 110 | putExtra(Intent.EXTRA_TEXT, "https://modrinth.com/user/$userId") 111 | type = "text/plain" 112 | } 113 | 114 | val shareIntent = Intent.createChooser(sendIntent, null) 115 | startActivity(shareIntent) 116 | 117 | return@setOnMenuItemClickListener true 118 | } 119 | else -> false 120 | } 121 | } 122 | 123 | return rootView 124 | } 125 | 126 | companion object { 127 | @JvmStatic 128 | fun newInstance() = 129 | UserFragment().apply { 130 | arguments = Bundle().apply { 131 | 132 | } 133 | } 134 | } 135 | } -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /app/src/main/java/me/theclashfruit/rithle/fragments/HomeFragment.kt: -------------------------------------------------------------------------------- 1 | package me.theclashfruit.rithle.fragments 2 | 3 | import android.os.Bundle 4 | import android.util.Log 5 | import androidx.fragment.app.Fragment 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import com.google.android.material.appbar.MaterialToolbar 10 | import com.google.android.material.bottomnavigation.BottomNavigationView 11 | import me.theclashfruit.rithle.classes.FilterBuilder 12 | import me.theclashfruit.rithle.R 13 | 14 | class HomeFragment : Fragment() { 15 | override fun onCreate(savedInstanceState: Bundle?) { 16 | super.onCreate(savedInstanceState) 17 | arguments?.let { 18 | 19 | } 20 | } 21 | 22 | override fun onCreateView( 23 | inflater: LayoutInflater, container: ViewGroup?, 24 | savedInstanceState: Bundle? 25 | ): View? { 26 | val rootView = inflater.inflate(R.layout.fragment_home, container, false) 27 | 28 | val bottomNav = rootView.findViewById(R.id.bottomNavigation) 29 | val toolBar = rootView.findViewById(R.id.toolbar) 30 | 31 | val filter = FilterBuilder() 32 | .setProjectType("mod") 33 | .addFilterItem("categories:'forge'") 34 | .addFilterItem("categories:'fabric'") 35 | .addFilterItem("categories:'quilt'") 36 | .addFilterItem("categories:'liteloader'") 37 | .addFilterItem("categories:'modloader'") 38 | .addFilterItem("categories:'rift'") 39 | .build() 40 | 41 | val fragmentTransaction = parentFragmentManager.beginTransaction() 42 | var listFragment = ListFragment.newInstance(filter) 43 | 44 | fragmentTransaction 45 | .replace(R.id.fragmentContainer, listFragment) 46 | .commit() 47 | 48 | Log.w("YesFilter", filter) 49 | 50 | toolBar.setOnMenuItemClickListener { item -> 51 | val toolBarFragmentTransaction = parentFragmentManager.beginTransaction() 52 | 53 | when(item.itemId) { 54 | R.id.toolBarSearch -> { 55 | return@setOnMenuItemClickListener true 56 | } 57 | R.id.toolBarAccount -> { 58 | val userFragment = UserFragment.newInstance() 59 | 60 | toolBarFragmentTransaction 61 | .addToBackStack("userFragment") 62 | .add(R.id.parentFragmentContainer, userFragment) 63 | .commit() 64 | 65 | return@setOnMenuItemClickListener true 66 | } 67 | R.id.toolBarSettings -> { 68 | val settingsFragment = SettingsContainerFragment.newInstance() 69 | 70 | toolBarFragmentTransaction 71 | .addToBackStack("settingsFragment") 72 | .add(R.id.parentFragmentContainer, settingsFragment) 73 | .commit() 74 | 75 | return@setOnMenuItemClickListener true 76 | } 77 | else -> false 78 | } 79 | } 80 | 81 | bottomNav.setOnNavigationItemSelectedListener { item -> 82 | val bottomNavFragmentTransaction = parentFragmentManager.beginTransaction() 83 | 84 | when(item.itemId) { 85 | R.id.itemMods -> { 86 | val fragmentFilter = FilterBuilder() 87 | .setProjectType("mod") 88 | .addFilterItem("categories:'forge'") 89 | .addFilterItem("categories:'fabric'") 90 | .addFilterItem("categories:'quilt'") 91 | .addFilterItem("categories:'liteloader'") 92 | .addFilterItem("categories:'modloader'") 93 | .addFilterItem("categories:'rift'") 94 | .build() 95 | 96 | listFragment = ListFragment.newInstance(fragmentFilter) 97 | 98 | bottomNavFragmentTransaction 99 | .replace(R.id.fragmentContainer, listFragment) 100 | .commit() 101 | 102 | return@setOnNavigationItemSelectedListener true 103 | } 104 | R.id.itemPlugins -> { 105 | val fragmentFilter = FilterBuilder() 106 | .setProjectType("mod") 107 | .addFilterItem("categories:'bukkit'") 108 | .addFilterItem("categories:'spigot'") 109 | .addFilterItem("categories:'paper'") 110 | .addFilterItem("categories:'purpur'") 111 | .addFilterItem("categories:'sponge'") 112 | .addFilterItem("categories:'bungeecord'") 113 | .addFilterItem("categories:'waterfall'") 114 | .addFilterItem("categories:'velocity'") 115 | .build() 116 | 117 | listFragment = ListFragment.newInstance(fragmentFilter) 118 | 119 | bottomNavFragmentTransaction 120 | .replace(R.id.fragmentContainer, listFragment) 121 | .commit() 122 | 123 | return@setOnNavigationItemSelectedListener true 124 | } 125 | R.id.itemResourcePacks -> { 126 | val fragmentFilter = FilterBuilder() 127 | .setProjectType("resourcepack") 128 | .build() 129 | 130 | listFragment = ListFragment.newInstance(fragmentFilter) 131 | 132 | bottomNavFragmentTransaction 133 | .replace(R.id.fragmentContainer, listFragment) 134 | .commit() 135 | 136 | return@setOnNavigationItemSelectedListener true 137 | } 138 | R.id.itemModpacks -> { 139 | val fragmentFilter = FilterBuilder() 140 | .setProjectType("modpack") 141 | .build() 142 | 143 | listFragment = ListFragment.newInstance(fragmentFilter) 144 | 145 | bottomNavFragmentTransaction 146 | .replace(R.id.fragmentContainer, listFragment) 147 | .commit() 148 | 149 | return@setOnNavigationItemSelectedListener true 150 | } 151 | else -> false 152 | } 153 | } 154 | 155 | return rootView 156 | } 157 | 158 | companion object { 159 | @JvmStatic 160 | fun newInstance() = 161 | HomeFragment().apply { 162 | arguments = Bundle().apply { 163 | 164 | } 165 | } 166 | } 167 | } -------------------------------------------------------------------------------- /app/src/main/java/me/theclashfruit/rithle/fragments/ProjectInfoFragment.kt: -------------------------------------------------------------------------------- 1 | package me.theclashfruit.rithle.fragments 2 | 3 | import android.content.Context 4 | import android.graphics.BitmapFactory 5 | import android.graphics.drawable.BitmapDrawable 6 | import android.net.Uri 7 | import android.os.Bundle 8 | import android.util.Log 9 | import android.view.LayoutInflater 10 | import android.view.View 11 | import android.view.ViewGroup 12 | import android.widget.ImageView 13 | import android.widget.TextView 14 | import androidx.browser.customtabs.CustomTabColorSchemeParams 15 | import androidx.browser.customtabs.CustomTabsIntent 16 | import androidx.core.content.ContextCompat 17 | import androidx.fragment.app.Fragment 18 | import com.android.volley.VolleyError 19 | import com.android.volley.toolbox.ImageLoader 20 | import io.noties.markwon.AbstractMarkwonPlugin 21 | import io.noties.markwon.Markwon 22 | import io.noties.markwon.MarkwonConfiguration 23 | import io.noties.markwon.ext.strikethrough.StrikethroughPlugin 24 | import io.noties.markwon.ext.tables.TablePlugin 25 | import io.noties.markwon.html.HtmlPlugin 26 | import io.noties.markwon.image.ImageItem 27 | import io.noties.markwon.image.ImagesPlugin 28 | import io.noties.markwon.linkify.LinkifyPlugin 29 | import kotlinx.serialization.decodeFromString 30 | import kotlinx.serialization.json.Json 31 | import me.theclashfruit.rithle.R 32 | import me.theclashfruit.rithle.classes.ProxySchemeHandler 33 | import me.theclashfruit.rithle.classes.RithleSingleton 34 | import me.theclashfruit.rithle.models.ModrinthProjectModel 35 | import java.net.URL 36 | import java.text.NumberFormat 37 | import java.util.* 38 | 39 | 40 | class ProjectInfoFragment : Fragment() { 41 | private var projectData: ModrinthProjectModel? = null 42 | 43 | override fun onCreate(savedInstanceState: Bundle?) { 44 | super.onCreate(savedInstanceState) 45 | 46 | val format = Json { ignoreUnknownKeys = true } 47 | 48 | arguments?.let { 49 | projectData = format.decodeFromString(it.getString("projectData")!!) 50 | } 51 | } 52 | 53 | override fun onCreateView( 54 | inflater: LayoutInflater, container: ViewGroup?, 55 | savedInstanceState: Bundle? 56 | ): View? { 57 | val rootView = inflater.inflate(R.layout.fragment_project_info, container, false) 58 | 59 | val projectIcon = rootView.findViewById(R.id.imageView) 60 | 61 | val nf = NumberFormat.getInstance(Locale.UK) 62 | 63 | val textViewTitle = rootView.findViewById(R.id.textViewTitle) 64 | val textViewDownloads = rootView.findViewById(R.id.textViewDownloads) 65 | val textViewFollowers = rootView.findViewById(R.id.textViewFollowers) 66 | 67 | textViewTitle.text = projectData!!.title 68 | textViewDownloads.text = resources.getQuantityString(R.plurals.project_downloads, projectData!!.downloads!!.toInt(), nf.format(projectData!!.downloads!!.toLong()).toString()) 69 | textViewFollowers.text = resources.getQuantityString(R.plurals.project_followers, projectData!!.followers!!.toInt(), nf.format(projectData!!.followers!!.toLong()).toString()) 70 | 71 | val markwon = Markwon.builder(requireContext()) 72 | .usePlugin(HtmlPlugin.create()) 73 | .usePlugin(TablePlugin.create(requireContext())) 74 | .usePlugin(StrikethroughPlugin.create()) 75 | .usePlugin(LinkifyPlugin.create()) 76 | .usePlugin(ImagesPlugin.create { plugin -> 77 | // for example to return a drawable resource 78 | plugin.addSchemeHandler(object : ProxySchemeHandler() { 79 | override fun handle(raw: String, uri: Uri): ImageItem { 80 | val sharedPref = activity!!.getSharedPreferences("me.theclashfruit.rithle_preferences", Context.MODE_PRIVATE) 81 | val doProxy = sharedPref!!.getBoolean("imageProxy", false) 82 | 83 | val url: URL = 84 | if(doProxy) 85 | URL("https://rthl.theclashfruit.me/img.php?url=$uri") 86 | else 87 | URL(uri.toString()) 88 | 89 | Log.d("ThatUrl", url.toString()) 90 | 91 | val image = BitmapFactory.decodeStream(url.openConnection().getInputStream()); 92 | 93 | return ImageItem.withResult(BitmapDrawable(resources, image)) 94 | } 95 | 96 | override fun supportedSchemes(): Collection { 97 | return listOf("http", "https") 98 | } 99 | }) 100 | }) 101 | .usePlugin(object : AbstractMarkwonPlugin() { 102 | override fun configureConfiguration(builder: MarkwonConfiguration.Builder) { 103 | builder.linkResolver { view, link -> 104 | view.callOnClick() 105 | 106 | Log.w("UriLog", view.paddingTop.toString()) 107 | Log.w("UriLog", link) 108 | 109 | CustomTabsIntent.Builder() 110 | .setColorScheme(CustomTabsIntent.COLOR_SCHEME_SYSTEM) 111 | .setColorSchemeParams( 112 | CustomTabsIntent.COLOR_SCHEME_DARK, 113 | CustomTabColorSchemeParams.Builder() 114 | .setToolbarColor(ContextCompat.getColor(requireContext(), R.color.colorPrimary)) 115 | .build() 116 | ) 117 | .setDefaultColorSchemeParams( 118 | CustomTabColorSchemeParams.Builder() 119 | .setToolbarColor(ContextCompat.getColor(requireContext(), R.color.colorPrimaryLight)) 120 | .build() 121 | ) 122 | .build() 123 | .launchUrl(requireContext(), Uri.parse(link)) 124 | } 125 | } 126 | }) 127 | .build() 128 | 129 | 130 | markwon.setParsedMarkdown(rootView.findViewById(R.id.textViewDescription), markwon.render(markwon.parse(projectData!!.body!!))) 131 | 132 | if(projectData!!.icon_url != null) { 133 | RithleSingleton.getInstance(requireContext()).imageLoader.get(projectData!!.icon_url.toString(), object : ImageLoader.ImageListener { 134 | override fun onResponse(response: ImageLoader.ImageContainer?, isImmediate: Boolean) { 135 | if (response != null) { 136 | projectIcon.setImageBitmap(response.bitmap) 137 | } 138 | } 139 | 140 | override fun onErrorResponse(error: VolleyError?) { 141 | Log.d("imageLoader", "wtf are you doing, you either don't have internet or the url is fucking wrong, btw the error is: ${error.toString()}") 142 | } 143 | }) 144 | } 145 | 146 | return rootView 147 | } 148 | 149 | companion object { 150 | @JvmStatic 151 | fun newInstance(projectDataString: String) = 152 | ProjectInfoFragment().apply { 153 | arguments = Bundle().apply { 154 | putString("projectData", projectDataString) 155 | } 156 | } 157 | } 158 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_project_info.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 13 | 14 | 18 | 19 | 23 | 24 | 27 | 28 | 29 | 41 | 42 | 46 | 47 | 57 | 58 | 63 | 64 | 65 | 66 | 79 | 80 | 91 | 92 | 103 | 104 | 105 | 106 | 118 | 119 | 123 | 124 | 140 | 141 | 142 | 143 | 144 | 156 | 157 | 161 | 162 | 178 | 179 | 180 | 181 | 182 | 195 | 196 | 200 | 201 | 215 | 216 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | --------------------------------------------------------------------------------