├── .github └── workflows │ └── gradle.yml ├── .gitignore ├── .idea ├── .gitignore ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── compiler.xml ├── gradle.xml ├── kotlinc.xml ├── misc.xml ├── render.experimental.xml └── vcs.xml ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── hamzaazman │ │ └── kotlinfreetoplay │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── hamzaazman │ │ │ └── kotlinfreetoplay │ │ │ ├── MainActivity.kt │ │ │ ├── common │ │ │ ├── Constant.kt │ │ │ ├── Extention.kt │ │ │ ├── NetworkResource.kt │ │ │ └── ViewBindingDelegate.kt │ │ │ ├── data │ │ │ ├── api │ │ │ │ └── GameApi.kt │ │ │ ├── datastore │ │ │ │ └── DataStoreRepository.kt │ │ │ ├── dto │ │ │ │ ├── GameDetailDto.kt │ │ │ │ ├── GameDto.kt │ │ │ │ ├── MinimumSystemRequirements.kt │ │ │ │ └── Screenshot.kt │ │ │ ├── mapper │ │ │ │ └── Mapper.kt │ │ │ └── repository │ │ │ │ └── GameRepositoryImpl.kt │ │ │ ├── di │ │ │ ├── Application.kt │ │ │ ├── NetworkModule.kt │ │ │ └── RepositoryModule.kt │ │ │ ├── domain │ │ │ ├── datastore │ │ │ │ └── DataStoreRepository.kt │ │ │ ├── model │ │ │ │ ├── GameDetailUi.kt │ │ │ │ └── GameUi.kt │ │ │ └── repository │ │ │ │ └── GameRepository.kt │ │ │ └── ui │ │ │ ├── detail │ │ │ ├── DetailFragment.kt │ │ │ ├── DetailUiState.kt │ │ │ ├── DetailViewModel.kt │ │ │ └── ReviewAdapter.kt │ │ │ └── home │ │ │ ├── GamePlatform.kt │ │ │ ├── HomeAdapter.kt │ │ │ ├── HomeFragment.kt │ │ │ ├── HomeUiState.kt │ │ │ └── HomeViewModel.kt │ └── res │ │ ├── anim │ │ └── recyclerview_alpha.xml │ │ ├── color │ │ ├── chip_select_state.xml │ │ └── chip_unselect_state.xml │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── baseline_filter_list.xml │ │ ├── browser.xml │ │ ├── category_shape.xml │ │ ├── expand_more_dark.xml │ │ ├── expand_more_white.xml │ │ ├── free_shape.xml │ │ ├── game_image_shape.xml │ │ ├── game_placeholder.xml │ │ ├── gradient_image_shape.xml │ │ ├── ic_arrow_back.xml │ │ ├── ic_dashboard_black_24dp.xml │ │ ├── ic_home_black_24dp.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_notifications_black_24dp.xml │ │ ├── shimmer_image_shape.xml │ │ └── windows.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── filter_drawer_menu.xml │ │ ├── fragment_detail.xml │ │ ├── fragment_home.xml │ │ ├── game_row_item.xml │ │ ├── review_row_item.xml │ │ ├── shimmer_detail_layout.xml │ │ ├── shimmer_game_row_item.xml │ │ └── shimmer_review_row_item.xml │ │ ├── menu │ │ ├── bottom_nav_menu.xml │ │ └── filter_menu.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── navigation │ │ └── mobile_navigation.xml │ │ ├── values-night │ │ └── themes.xml │ │ ├── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ ├── data_extraction_rules.xml │ │ └── datastore_preference.xml │ └── test │ └── java │ └── com │ └── hamzaazman │ └── kotlinfreetoplay │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── screenshots ├── detail_dark.png ├── detail_light.png ├── freetogame_preview.png ├── home_dark.png └── home_light.png └── settings.gradle /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | # This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time 6 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle 7 | 8 | name: Java CI with Gradle 9 | 10 | on: 11 | push: 12 | branches: 13 | - '*' 14 | paths-ignore: 15 | - '**.md' 16 | - '**.yml' 17 | pull_request: 18 | branches: 19 | - '*' 20 | paths-ignore: 21 | - '**.md' 22 | types: [opened, reopened] 23 | workflow_dispatch: 24 | 25 | permissions: 26 | contents: read 27 | 28 | jobs: 29 | build: 30 | runs-on: ubuntu-latest 31 | 32 | steps: 33 | - uses: actions/checkout@v3 34 | - name: Set up Java 17 35 | uses: actions/setup-java@v3 36 | with: 37 | java-version: 17 38 | distribution: 'adopt' 39 | cache: gradle 40 | 41 | - name: Cache Gradle dependencies 42 | uses: actions/cache@v2 43 | with: 44 | path: ~/.gradle/caches 45 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} 46 | restore-keys: | 47 | ${{ runner.os }}-gradle- 48 | 49 | - name: Grant execute permission for gradlew 50 | run: chmod +x gradlew 51 | 52 | - name: Build Debug APK 53 | run: ./gradlew assembleDebug 54 | 55 | - name: Upload APK artifact 56 | uses: actions/upload-artifact@v3 57 | 58 | with: 59 | name: Upload the APK 60 | path: app/build/outputs/apk/debug/app-debug*.apk 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | .idea 4 | /local.properties 5 | /.idea/caches 6 | /.idea/libraries 7 | /.idea/modules.xml 8 | /.idea/workspace.xml 9 | /.idea/navEditor.xml 10 | /.idea/assetWizardSettings.xml 11 | .DS_Store 12 | /build 13 | /captures 14 | .externalNativeBuild 15 | .cxx 16 | local.properties 17 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 119 | 120 | 122 | 123 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 19 | 20 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | 12 | -------------------------------------------------------------------------------- /.idea/render.experimental.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Hamza Azman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Free to Game 2 | 3 | [![](https://img.shields.io/github/downloads/hamzaazman/kotlinfreetogame/total?color=orange&logo=github&logoColor=white)](https://tooomm.github.io/github-release-stats/?username=hamzaazman&repository=KotlinFreeToGame) 4 | [![Hits](https://hits.seeyoufarm.com/api/count/incr/badge.svg?url=https://github.com/hamzaazman/KotlinFreeToGame&count_bg=%239A3DC8&title_bg=%23555555&icon=tencentweibo.svg&icon_color=%23E7E7E7&title=Visitors&edge_flat=false)](https://hits.seeyoufarm.com) 5 | [![Release](https://img.shields.io/github/v/release/hamzaazman/KotlinFreeToGame?color=52be80&label=Release)](https://github.com/hamzaazman/KotlinFreeToGame/releases) 6 | ![](https://img.shields.io/badge/Minimum%20SDK-24%20(Marshmallow)-839192?logo=android&logoColor=white) 7 | ![](https://img.shields.io/badge/Target%20SDK-33%20(Android%2013)-566573?logo=android&logoColor=white) 8 | ![](https://img.shields.io/github/license/hamzaazman/KotlinFreeToGame?color=red&label=License) 9 | 10 | Kotlin Free To Game is an Android app that allows users to explore a variety of free-to-play games.The app utilizes the RAWG Video Games Database API to fetch the latest game data. It follows modern Android development practices, including MVVM architecture, Jetpack components. 11 | 12 | 13 | 14 | # Tech Stack 15 | - **Jetpack** 16 | - **Flow**: Flow is conceptually a stream of data that can be computed asynchronously. 17 | - **Lifecycle**: Lifecycle is a series of callbacks executed in a certain order when the status of the activity or fragment changes. 18 | - **View Binding**: View binding is a feature that allows us to more easily write code that interacts with views. 19 | - **ViewModel**: ViewModel class is designed to hold and manage UI-related data in a life-cycle conscious way. This allows data to survive configuration changes such as screen rotations. 20 | - **Hilt**: Hilt is a dependency injection library for Android 21 | - **Coil**: Image loading for android by kotlin coroutines 22 | - **Navigation Components**: Navigation is a framework for navigating between destinations within an Android application that provides a consistent API whether destinations are implemented as Fragments, Activities, or other components. 23 | - **Retrofit**: Retrofit is a type-safe REST client developed by Square for Android, Java, and Kotlin. 24 | 25 | 26 | # License 27 | 28 | ``` 29 | MIT License 30 | 31 | Copyright (c) 2023 Hamza Azman 32 | 33 | Permission is hereby granted, free of charge, to any person obtaining a copy of 34 | this software and associated documentation files (the "Software"), to deal 35 | in the Software without restriction, including without limitation the rights to use, 36 | copy, modify, merge, publish, distribute, sublicense, and/or sell 37 | copies of the Software, and to permit persons to whom the Software is furnished to do so, 38 | subject to the following conditions: 39 | 40 | The above copyright notice and this permission notice shall be included in 41 | all copies or substantial portions of the Software. 42 | 43 | ``` 44 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'org.jetbrains.kotlin.android' 4 | id 'kotlin-kapt' 5 | id 'com.google.dagger.hilt.android' 6 | id 'kotlin-parcelize' 7 | id 'androidx.navigation.safeargs.kotlin' 8 | } 9 | 10 | android { 11 | namespace 'com.hamzaazman.kotlinfreetoplay' 12 | compileSdk 33 13 | 14 | defaultConfig { 15 | applicationId "com.hamzaazman.kotlinfreetoplay" 16 | minSdk 24 17 | targetSdk 33 18 | versionCode 1 19 | versionName "1.0" 20 | 21 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 22 | } 23 | 24 | 25 | buildTypes { 26 | release { 27 | minifyEnabled false 28 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 29 | } 30 | } 31 | compileOptions { 32 | sourceCompatibility JavaVersion.VERSION_17 33 | targetCompatibility JavaVersion.VERSION_17 34 | } 35 | kotlinOptions { 36 | jvmTarget = '17' 37 | } 38 | buildFeatures { 39 | viewBinding true 40 | } 41 | } 42 | 43 | dependencies { 44 | 45 | implementation 'androidx.core:core-ktx:1.10.1' 46 | implementation 'androidx.appcompat:appcompat:1.6.1' 47 | implementation 'com.google.android.material:material:1.9.0' 48 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4' 49 | implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1' 50 | implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1' 51 | implementation 'androidx.navigation:navigation-fragment-ktx:2.5.3' 52 | implementation 'androidx.navigation:navigation-ui-ktx:2.5.3' 53 | implementation 'androidx.legacy:legacy-support-v4:1.0.0' 54 | testImplementation 'junit:junit:4.13.2' 55 | androidTestImplementation 'androidx.test.ext:junit:1.1.5' 56 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' 57 | 58 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1" 59 | implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.1" 60 | implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.1" 61 | 62 | implementation 'com.squareup.retrofit2:retrofit:2.9.0' 63 | implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.5' 64 | 65 | implementation 'com.squareup.retrofit2:converter-gson:2.9.0' 66 | implementation 'com.squareup.retrofit2:converter-moshi:2.9.0' 67 | 68 | 69 | implementation 'com.google.dagger:hilt-android:2.46.1' 70 | kapt 'com.google.dagger:hilt-compiler:2.46.1' 71 | 72 | implementation("io.coil-kt:coil:2.3.0") 73 | 74 | implementation "com.github.skydoves:expandablelayout:1.0.7" 75 | 76 | implementation "androidx.datastore:datastore-preferences:1.0.0" 77 | implementation "androidx.datastore:datastore-preferences-core:1.0.0" 78 | 79 | implementation 'com.facebook.shimmer:shimmer:0.5.0' 80 | 81 | } -------------------------------------------------------------------------------- /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/androidTest/java/com/hamzaazman/kotlinfreetoplay/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.hamzaazman.kotlinfreetoplay 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("com.hamzaazman.kotlinfreetoplay", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 18 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/hamzaazman/kotlinfreetoplay/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.hamzaazman.kotlinfreetoplay 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import androidx.navigation.NavController 6 | import androidx.navigation.fragment.NavHostFragment 7 | import com.hamzaazman.kotlinfreetoplay.databinding.ActivityMainBinding 8 | import dagger.hilt.android.AndroidEntryPoint 9 | 10 | @AndroidEntryPoint 11 | class MainActivity : AppCompatActivity() { 12 | 13 | private lateinit var binding: ActivityMainBinding 14 | private lateinit var navController: NavController 15 | 16 | override fun onCreate(savedInstanceState: Bundle?) { 17 | super.onCreate(savedInstanceState) 18 | 19 | 20 | binding = ActivityMainBinding.inflate(layoutInflater) 21 | setContentView(binding.root) 22 | 23 | val navHostFragment = 24 | supportFragmentManager.findFragmentById(R.id.fragmentContainerView) as NavHostFragment 25 | navController = navHostFragment.navController 26 | 27 | } 28 | 29 | override fun onSupportNavigateUp(): Boolean { 30 | return super.onSupportNavigateUp() || navController.navigateUp() 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hamzaazman/kotlinfreetoplay/common/Constant.kt: -------------------------------------------------------------------------------- 1 | package com.hamzaazman.kotlinfreetoplay.common 2 | 3 | object Constant { 4 | const val BASE_URL = "https://www.freetogame.com/" 5 | //const val BASE_URL = "https://raw.githubusercontent.com/hamzaazman/Dataset/main/" 6 | 7 | const val CATEGORY_PREFERENCES = "category_preferences" 8 | const val CATEGORY_PREF_KEY = "category_key" 9 | const val CATEGORY_ID_PREF_KEY = "category_id_key" 10 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hamzaazman/kotlinfreetoplay/common/Extention.kt: -------------------------------------------------------------------------------- 1 | package com.hamzaazman.kotlinfreetoplay 2 | 3 | import android.graphics.Rect 4 | import android.transition.TransitionManager 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.widget.ImageView 8 | import android.widget.TextView 9 | import androidx.recyclerview.widget.RecyclerView 10 | import java.text.ParseException 11 | import java.text.SimpleDateFormat 12 | import java.util.Locale 13 | 14 | fun RecyclerView.addVerticalMarginDecoration(margin: Int) { 15 | addItemDecoration(object : RecyclerView.ItemDecoration() { 16 | override fun getItemOffsets( 17 | outRect: Rect, 18 | view: View, 19 | parent: RecyclerView, 20 | state: RecyclerView.State 21 | ) { 22 | if (parent.getChildAdapterPosition(view) != parent.adapter!!.itemCount - 1) { 23 | outRect.top = margin 24 | outRect.bottom = margin 25 | } 26 | } 27 | }) 28 | } 29 | 30 | fun RecyclerView.addHorizontalMarginDecoration(margin: Int) { 31 | addItemDecoration(object : RecyclerView.ItemDecoration() { 32 | override fun getItemOffsets( 33 | outRect: Rect, 34 | view: View, 35 | parent: RecyclerView, 36 | state: RecyclerView.State 37 | ) { 38 | if (parent.getChildAdapterPosition(view) != parent.adapter!!.itemCount - 1) { 39 | outRect.right = margin 40 | outRect.left = margin 41 | } 42 | } 43 | }) 44 | } 45 | 46 | fun RecyclerView.customItemDecoration(padding: Int) { 47 | addItemDecoration(object : RecyclerView.ItemDecoration() { 48 | override fun getItemOffsets( 49 | outRect: Rect, 50 | view: View, 51 | parent: RecyclerView, 52 | state: RecyclerView.State 53 | ) { 54 | if (parent.getChildAdapterPosition(view) != parent.adapter!!.itemCount - 1) { 55 | outRect.bottom = padding 56 | } 57 | } 58 | }) 59 | } 60 | 61 | 62 | class VerticalItemDecoration(private val margin: Int) : RecyclerView.ItemDecoration() { 63 | 64 | override fun getItemOffsets( 65 | outRect: Rect, 66 | view: View, 67 | parent: RecyclerView, 68 | state: RecyclerView.State 69 | ) { 70 | outRect.apply { 71 | bottom = margin 72 | top = margin 73 | } 74 | } 75 | } 76 | 77 | class HorizontalItemDecoration(private val margin: Int) : RecyclerView.ItemDecoration() { 78 | 79 | override fun getItemOffsets( 80 | outRect: Rect, 81 | view: View, 82 | parent: RecyclerView, 83 | state: RecyclerView.State 84 | ) { 85 | outRect.apply { 86 | left = margin 87 | right = margin 88 | } 89 | } 90 | } 91 | 92 | fun String.capitalizeFirstLetter(): String { 93 | return replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.ROOT) else it.toString() } 94 | } 95 | 96 | fun String.extractYearFromDateString(): String? { 97 | val date = try { 98 | SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse(this) 99 | } catch (e: ParseException) { 100 | e.printStackTrace() 101 | return null 102 | } 103 | return SimpleDateFormat("yyyy", Locale.getDefault()).format(date) 104 | } 105 | 106 | fun TextView.makeCollapsible( 107 | maxLinesCollapsed: Int, 108 | maxLinesExpanded: Int, 109 | expandMoreDrawable: ImageView 110 | ) { 111 | maxLines = maxLinesCollapsed 112 | 113 | setOnClickListener { 114 | maxLines = if (maxLines == maxLinesCollapsed) { 115 | expandMoreDrawable.rotation = 180f 116 | maxLinesExpanded 117 | } else { 118 | expandMoreDrawable.rotation = 0f 119 | maxLinesCollapsed 120 | } 121 | TransitionManager.beginDelayedTransition(parent as ViewGroup) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /app/src/main/java/com/hamzaazman/kotlinfreetoplay/common/NetworkResource.kt: -------------------------------------------------------------------------------- 1 | package com.hamzaazman.kotlinfreetoplay.common 2 | 3 | sealed class NetworkResource { 4 | object Loading : NetworkResource() 5 | data class Success(val data: T) : NetworkResource() 6 | data class Error(val throwable: String?) : NetworkResource() 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hamzaazman/kotlinfreetoplay/common/ViewBindingDelegate.kt: -------------------------------------------------------------------------------- 1 | package com.hamzaazman.kotlinfreetoplay.common 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import androidx.appcompat.app.AppCompatActivity 7 | import androidx.fragment.app.DialogFragment 8 | import androidx.fragment.app.Fragment 9 | import androidx.lifecycle.DefaultLifecycleObserver 10 | import androidx.lifecycle.Lifecycle 11 | import androidx.lifecycle.LifecycleOwner 12 | import androidx.viewbinding.ViewBinding 13 | import kotlin.properties.ReadOnlyProperty 14 | import kotlin.reflect.KProperty 15 | 16 | /** Activity binding delegate, may be used since onCreate up to onDestroy (inclusive) */ 17 | inline fun AppCompatActivity.viewBinding(crossinline factory: (LayoutInflater) -> T) = 18 | lazy(LazyThreadSafetyMode.NONE) { 19 | factory(layoutInflater) 20 | } 21 | 22 | /** Fragment binding delegate, may be used since onViewCreated up to onDestroyView (inclusive) */ 23 | fun Fragment.viewBinding(factory: (View) -> T): ReadOnlyProperty = 24 | object : ReadOnlyProperty, DefaultLifecycleObserver { 25 | private var binding: T? = null 26 | 27 | override fun getValue(thisRef: Fragment, property: KProperty<*>): T = 28 | binding ?: factory(requireView()).also { 29 | // if binding is accessed after Lifecycle is DESTROYED, create new instance, but don't cache it 30 | if (viewLifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) { 31 | viewLifecycleOwner.lifecycle.addObserver(this) 32 | binding = it 33 | } 34 | } 35 | 36 | override fun onDestroy(owner: LifecycleOwner) { 37 | binding = null 38 | } 39 | } 40 | 41 | /** Binding delegate for DialogFragments implementing onCreateDialog (like Activities, they don't 42 | * have a separate view lifecycle), may be used since onCreateDialog up to onDestroy (inclusive) */ 43 | inline fun DialogFragment.viewBinding(crossinline factory: (LayoutInflater) -> T) = 44 | lazy(LazyThreadSafetyMode.NONE) { 45 | factory(layoutInflater) 46 | } 47 | 48 | /** Not really a delegate, just a small helper for RecyclerView.ViewHolders */ 49 | inline fun ViewGroup.viewBinding(factory: (LayoutInflater, ViewGroup, Boolean) -> T) = 50 | factory(LayoutInflater.from(context), this, false) -------------------------------------------------------------------------------- /app/src/main/java/com/hamzaazman/kotlinfreetoplay/data/api/GameApi.kt: -------------------------------------------------------------------------------- 1 | package com.hamzaazman.kotlinfreetoplay.data.api 2 | 3 | import com.hamzaazman.kotlinfreetoplay.data.dto.GameDetailDto 4 | import com.hamzaazman.kotlinfreetoplay.data.dto.GameDto 5 | import retrofit2.http.GET 6 | import retrofit2.http.Path 7 | import retrofit2.http.Query 8 | 9 | interface GameApi { 10 | 11 | @GET("api/games") 12 | //@GET("games.json") 13 | suspend fun getAllGame(): List 14 | 15 | @GET("api/games/") 16 | suspend fun getGameByCategory( 17 | @Query("category") category: String 18 | ): List 19 | 20 | @GET("api/game") 21 | suspend fun getGameDetailById( 22 | @Query("id") gameId: Int 23 | ): GameDetailDto 24 | 25 | 26 | 27 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hamzaazman/kotlinfreetoplay/data/datastore/DataStoreRepository.kt: -------------------------------------------------------------------------------- 1 | package com.hamzaazman.kotlinfreetoplay.data.datastore 2 | 3 | import android.content.Context 4 | import android.util.Log 5 | import androidx.datastore.core.DataStore 6 | import androidx.datastore.preferences.core.Preferences 7 | import androidx.datastore.preferences.core.edit 8 | import androidx.datastore.preferences.core.emptyPreferences 9 | import androidx.datastore.preferences.core.intPreferencesKey 10 | import androidx.datastore.preferences.core.stringPreferencesKey 11 | import androidx.datastore.preferences.preferencesDataStore 12 | import com.hamzaazman.kotlinfreetoplay.common.Constant.CATEGORY_ID_PREF_KEY 13 | import com.hamzaazman.kotlinfreetoplay.common.Constant.CATEGORY_PREFERENCES 14 | import com.hamzaazman.kotlinfreetoplay.common.Constant.CATEGORY_PREF_KEY 15 | import dagger.hilt.android.qualifiers.ApplicationContext 16 | import dagger.hilt.android.scopes.ActivityRetainedScoped 17 | import kotlinx.coroutines.flow.Flow 18 | import kotlinx.coroutines.flow.catch 19 | import kotlinx.coroutines.flow.first 20 | import kotlinx.coroutines.flow.map 21 | import java.io.IOException 22 | import javax.inject.Inject 23 | 24 | 25 | @ActivityRetainedScoped 26 | class DataStoreRepositoryImpl @Inject constructor( 27 | @ApplicationContext private val context: Context 28 | ) { 29 | 30 | private object PreferenceKeys { 31 | val checkedCategory = stringPreferencesKey(CATEGORY_PREF_KEY) 32 | val checkedCategoryId = intPreferencesKey(CATEGORY_ID_PREF_KEY) 33 | } 34 | 35 | private val Context.dataStore: DataStore by preferencesDataStore(name = CATEGORY_PREFERENCES) 36 | 37 | suspend fun saveCategoryAndId( 38 | category: String, categoryId: Int 39 | ) { 40 | context.dataStore.edit { preferences -> 41 | preferences[PreferenceKeys.checkedCategory] = category 42 | preferences[PreferenceKeys.checkedCategoryId] = categoryId 43 | } 44 | } 45 | 46 | val getCategoryAndId: Flow = context.dataStore.data 47 | .catch { exception -> 48 | if (exception is IOException) { 49 | emit(emptyPreferences()) 50 | } else { 51 | throw exception 52 | } 53 | } 54 | .map { preferences -> 55 | val checkedCategory = preferences[PreferenceKeys.checkedCategory] ?: "home" 56 | val checkedCategoryId = preferences[PreferenceKeys.checkedCategoryId] ?: 0 57 | CategoryType( 58 | checkedCategory = checkedCategory, 59 | checkedCategoryId = checkedCategoryId 60 | ) 61 | } 62 | 63 | suspend fun clearCategory() = context.dataStore.edit { it.clear() } 64 | suspend fun isCategoryDataStoreEmpty(): Boolean { 65 | val categoryType = getCategoryAndId.first() 66 | Log.d("VM", "selectCategory: $categoryType") 67 | return categoryType.checkedCategory == "all" && categoryType.checkedCategoryId == 0 68 | } 69 | 70 | } 71 | 72 | data class CategoryType( 73 | val checkedCategory: String, 74 | val checkedCategoryId: Int, 75 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/hamzaazman/kotlinfreetoplay/data/dto/GameDetailDto.kt: -------------------------------------------------------------------------------- 1 | package com.hamzaazman.kotlinfreetoplay.data.dto 2 | 3 | data class GameDetailDto( 4 | val description: String, 5 | val developer: String, 6 | val freetogame_profile_url: String, 7 | val game_url: String, 8 | val genre: String, 9 | val id: Int, 10 | val minimum_system_requirements: MinimumSystemRequirements, 11 | val platform: String, 12 | val publisher: String, 13 | val release_date: String, 14 | val screenshots: List, 15 | val short_description: String, 16 | val status: String, 17 | val thumbnail: String, 18 | val title: String 19 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/hamzaazman/kotlinfreetoplay/data/dto/GameDto.kt: -------------------------------------------------------------------------------- 1 | package com.hamzaazman.kotlinfreetoplay.data.dto 2 | 3 | data class GameDto( 4 | val developer: String, 5 | val freetogame_profile_url: String, 6 | val game_url: String, 7 | val genre: String, 8 | val id: Int, 9 | val platform: String, 10 | val publisher: String, 11 | val release_date: String, 12 | val short_description: String, 13 | val thumbnail: String, 14 | val title: String 15 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/hamzaazman/kotlinfreetoplay/data/dto/MinimumSystemRequirements.kt: -------------------------------------------------------------------------------- 1 | package com.hamzaazman.kotlinfreetoplay.data.dto 2 | 3 | data class MinimumSystemRequirements( 4 | val graphics: String?, 5 | val memory: String?, 6 | val os: String?, 7 | val processor: String?, 8 | val storage: String? 9 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/hamzaazman/kotlinfreetoplay/data/dto/Screenshot.kt: -------------------------------------------------------------------------------- 1 | package com.hamzaazman.kotlinfreetoplay.data.dto 2 | 3 | 4 | data class Screenshot( 5 | val id: Int, 6 | val image: String 7 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/hamzaazman/kotlinfreetoplay/data/mapper/Mapper.kt: -------------------------------------------------------------------------------- 1 | package com.hamzaazman.kotlinfreetoplay.data.mapper 2 | 3 | import com.hamzaazman.kotlinfreetoplay.data.dto.GameDetailDto 4 | import com.hamzaazman.kotlinfreetoplay.data.dto.GameDto 5 | import com.hamzaazman.kotlinfreetoplay.domain.model.GameDetailUi 6 | import com.hamzaazman.kotlinfreetoplay.domain.model.GameUi 7 | 8 | 9 | fun toDomain(dto: GameDto): GameUi { 10 | return GameUi( 11 | id = dto.id, 12 | title = dto.title.orEmpty(), 13 | game_url = dto.game_url.orEmpty(), 14 | platform = dto.platform.orEmpty(), 15 | genre = dto.genre.orEmpty(), 16 | release_date = dto.release_date.orEmpty(), 17 | short_description = dto.short_description.orEmpty(), 18 | thumbnail = dto.thumbnail.orEmpty() 19 | ) 20 | } 21 | fun detailToDomain(detailDto: GameDetailDto): GameDetailUi { 22 | return GameDetailUi( 23 | id = detailDto.id, 24 | title = detailDto.title.orEmpty(), 25 | thumbnail = detailDto.thumbnail.orEmpty(), 26 | publisher = detailDto.publisher.orEmpty(), 27 | platform = detailDto.platform.orEmpty(), 28 | description = detailDto.description.orEmpty(), 29 | gameUrl = detailDto.game_url.orEmpty(), 30 | genre = detailDto.genre.orEmpty(), 31 | freetogameProfile_url = detailDto.freetogame_profile_url.orEmpty(), 32 | minimumSystemRequirements = detailDto.minimum_system_requirements, 33 | releaseDate = detailDto.release_date.orEmpty(), 34 | screenshots = detailDto.screenshots.orEmpty(), 35 | ) 36 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hamzaazman/kotlinfreetoplay/data/repository/GameRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package com.hamzaazman.kotlinfreetoplay.data.repository 2 | 3 | import com.hamzaazman.kotlinfreetoplay.common.NetworkResource 4 | import com.hamzaazman.kotlinfreetoplay.data.api.GameApi 5 | import com.hamzaazman.kotlinfreetoplay.data.mapper.detailToDomain 6 | import com.hamzaazman.kotlinfreetoplay.data.mapper.toDomain 7 | import com.hamzaazman.kotlinfreetoplay.domain.model.GameDetailUi 8 | import com.hamzaazman.kotlinfreetoplay.domain.model.GameUi 9 | import com.hamzaazman.kotlinfreetoplay.domain.repository.GameRepository 10 | import kotlinx.coroutines.Dispatchers 11 | import kotlinx.coroutines.flow.Flow 12 | import kotlinx.coroutines.flow.flow 13 | import kotlinx.coroutines.flow.flowOn 14 | import okio.IOException 15 | import retrofit2.HttpException 16 | import javax.inject.Inject 17 | 18 | class GameRepositoryImpl @Inject constructor( 19 | private val api: GameApi 20 | ) : GameRepository { 21 | override suspend fun getAllGame(): Flow>> = flow { 22 | emit(NetworkResource.Loading) 23 | try { 24 | val response = api.getAllGame() 25 | emit(NetworkResource.Success(data = response.map { toDomain(it) })) 26 | } catch (e: HttpException) { 27 | e.printStackTrace() 28 | emit(NetworkResource.Error(e.localizedMessage?.toString())) 29 | } catch (e: IOException) { 30 | e.printStackTrace() 31 | emit(NetworkResource.Error(e.localizedMessage?.toString())) 32 | } 33 | }.flowOn(Dispatchers.IO) 34 | 35 | override suspend fun getGameByCategory(category: String): Flow>> = 36 | flow { 37 | emit(NetworkResource.Loading) 38 | try { 39 | val response = api.getGameByCategory(category = category) 40 | emit(NetworkResource.Success(data = response.map { toDomain(it) })) 41 | } catch (e: HttpException) { 42 | e.printStackTrace() 43 | emit(NetworkResource.Error(e.localizedMessage?.toString())) 44 | } catch (e: IOException) { 45 | e.printStackTrace() 46 | emit(NetworkResource.Error(e.localizedMessage?.toString())) 47 | } 48 | }.flowOn(Dispatchers.IO) 49 | 50 | override suspend fun getGameDetailById(id: Int): Flow> = 51 | flow { 52 | emit(NetworkResource.Loading) 53 | try { 54 | val response = api.getGameDetailById(id) 55 | emit(NetworkResource.Success(data = detailToDomain(response))) 56 | } catch (e: HttpException) { 57 | e.printStackTrace() 58 | emit(NetworkResource.Error(e.localizedMessage?.toString())) 59 | } catch (e: IOException) { 60 | e.printStackTrace() 61 | emit(NetworkResource.Error(e.localizedMessage?.toString())) 62 | } 63 | }.flowOn(Dispatchers.IO) 64 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hamzaazman/kotlinfreetoplay/di/Application.kt: -------------------------------------------------------------------------------- 1 | package com.hamzaazman.kotlinfreetoplay.di 2 | 3 | import android.app.Application 4 | import dagger.hilt.android.HiltAndroidApp 5 | 6 | @HiltAndroidApp 7 | class Application : Application() { 8 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hamzaazman/kotlinfreetoplay/di/NetworkModule.kt: -------------------------------------------------------------------------------- 1 | package com.hamzaazman.kotlinfreetoplay.di 2 | 3 | import com.google.gson.Gson 4 | import com.google.gson.GsonBuilder 5 | import com.hamzaazman.kotlinfreetoplay.common.Constant.BASE_URL 6 | import com.hamzaazman.kotlinfreetoplay.data.api.GameApi 7 | import dagger.Module 8 | import dagger.Provides 9 | import dagger.hilt.InstallIn 10 | import dagger.hilt.components.SingletonComponent 11 | import okhttp3.OkHttpClient 12 | import okhttp3.logging.HttpLoggingInterceptor 13 | import retrofit2.Retrofit 14 | import retrofit2.converter.gson.GsonConverterFactory 15 | import java.util.concurrent.TimeUnit 16 | import javax.inject.Singleton 17 | 18 | @Module 19 | @InstallIn(SingletonComponent::class) 20 | object NetworkModule { 21 | 22 | @Singleton 23 | @Provides 24 | fun provideLoggingInterceptor(): HttpLoggingInterceptor { 25 | return HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY) 26 | } 27 | 28 | @Provides 29 | @Singleton 30 | fun provideOkHttpClient(inter: HttpLoggingInterceptor): OkHttpClient { 31 | return OkHttpClient.Builder() 32 | .addInterceptor(inter) 33 | .connectTimeout(30, TimeUnit.SECONDS) 34 | .readTimeout( 35 | 30, 36 | TimeUnit.SECONDS 37 | ) 38 | .writeTimeout( 39 | 30, 40 | TimeUnit.SECONDS 41 | ).build() 42 | } 43 | 44 | @Singleton 45 | @Provides 46 | fun provideConverterFactory(): Gson { 47 | return GsonBuilder().setLenient().create() 48 | } 49 | 50 | @Singleton 51 | @Provides 52 | fun provideRetrofit( 53 | okHttpClient: OkHttpClient, 54 | gson: Gson 55 | ): Retrofit = 56 | Retrofit.Builder().baseUrl(BASE_URL).client(okHttpClient) 57 | .addConverterFactory(GsonConverterFactory.create(gson)).build() 58 | 59 | @Singleton 60 | @Provides 61 | fun provide(retrofit: Retrofit): GameApi = retrofit.create(GameApi::class.java) 62 | 63 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hamzaazman/kotlinfreetoplay/di/RepositoryModule.kt: -------------------------------------------------------------------------------- 1 | package com.hamzaazman.kotlinfreetoplay.di 2 | 3 | import com.hamzaazman.kotlinfreetoplay.data.api.GameApi 4 | import com.hamzaazman.kotlinfreetoplay.data.repository.GameRepositoryImpl 5 | import dagger.Module 6 | import dagger.Provides 7 | import dagger.hilt.InstallIn 8 | import dagger.hilt.components.SingletonComponent 9 | import javax.inject.Singleton 10 | 11 | @Module 12 | @InstallIn(SingletonComponent::class) 13 | object RepositoryModule { 14 | 15 | @Provides 16 | @Singleton 17 | fun provideGameRepository(gameApi: GameApi): GameRepositoryImpl { 18 | return GameRepositoryImpl(gameApi) 19 | } 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hamzaazman/kotlinfreetoplay/domain/datastore/DataStoreRepository.kt: -------------------------------------------------------------------------------- 1 | package com.hamzaazman.kotlinfreetoplay.domain.datastore 2 | 3 | interface DataStoreRepository { 4 | suspend fun putString(key: String, value: String) 5 | suspend fun getString(key: String): String? 6 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hamzaazman/kotlinfreetoplay/domain/model/GameDetailUi.kt: -------------------------------------------------------------------------------- 1 | package com.hamzaazman.kotlinfreetoplay.domain.model 2 | 3 | import com.google.gson.annotations.SerializedName 4 | import com.hamzaazman.kotlinfreetoplay.data.dto.MinimumSystemRequirements 5 | import com.hamzaazman.kotlinfreetoplay.data.dto.Screenshot 6 | 7 | data class GameDetailUi( 8 | val description: String?, 9 | @SerializedName("freetogame_profile_url") 10 | val freetogameProfile_url: String?, 11 | @SerializedName("game_url") 12 | val gameUrl: String?, 13 | val genre: String?, 14 | val id: Int, 15 | @SerializedName("minimum_system_requirements") 16 | val minimumSystemRequirements: MinimumSystemRequirements?, 17 | val platform: String?, 18 | val publisher: String?, 19 | @SerializedName("release_date") 20 | val releaseDate: String?, 21 | val screenshots: List?, 22 | val thumbnail: String?, 23 | val title: String? 24 | ) 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/hamzaazman/kotlinfreetoplay/domain/model/GameUi.kt: -------------------------------------------------------------------------------- 1 | package com.hamzaazman.kotlinfreetoplay.domain.model 2 | 3 | data class GameUi( 4 | val id: Int, 5 | val platform: String, 6 | val release_date: String, 7 | val short_description: String, 8 | val genre: String, 9 | val thumbnail: String, 10 | val title: String, 11 | val game_url: String, 12 | ) 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/hamzaazman/kotlinfreetoplay/domain/repository/GameRepository.kt: -------------------------------------------------------------------------------- 1 | package com.hamzaazman.kotlinfreetoplay.domain.repository 2 | 3 | import com.hamzaazman.kotlinfreetoplay.common.NetworkResource 4 | import com.hamzaazman.kotlinfreetoplay.domain.model.GameDetailUi 5 | import com.hamzaazman.kotlinfreetoplay.domain.model.GameUi 6 | import kotlinx.coroutines.flow.Flow 7 | 8 | interface GameRepository { 9 | suspend fun getAllGame(): Flow>> 10 | suspend fun getGameByCategory(category: String): Flow>> 11 | suspend fun getGameDetailById(id: Int): Flow> 12 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hamzaazman/kotlinfreetoplay/ui/detail/DetailFragment.kt: -------------------------------------------------------------------------------- 1 | package com.hamzaazman.kotlinfreetoplay.ui.detail 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import androidx.core.widget.NestedScrollView 6 | import androidx.fragment.app.Fragment 7 | import androidx.fragment.app.viewModels 8 | import androidx.lifecycle.Lifecycle 9 | import androidx.lifecycle.lifecycleScope 10 | import androidx.lifecycle.repeatOnLifecycle 11 | import androidx.navigation.fragment.findNavController 12 | import androidx.navigation.fragment.navArgs 13 | import coil.load 14 | import com.hamzaazman.kotlinfreetoplay.R 15 | import com.hamzaazman.kotlinfreetoplay.common.viewBinding 16 | import com.hamzaazman.kotlinfreetoplay.databinding.FragmentDetailBinding 17 | import com.hamzaazman.kotlinfreetoplay.domain.model.GameDetailUi 18 | import com.hamzaazman.kotlinfreetoplay.extractYearFromDateString 19 | import com.hamzaazman.kotlinfreetoplay.makeCollapsible 20 | import dagger.hilt.android.AndroidEntryPoint 21 | import kotlinx.coroutines.launch 22 | 23 | 24 | @AndroidEntryPoint 25 | class DetailFragment : Fragment(R.layout.fragment_detail) { 26 | private val binding by viewBinding(FragmentDetailBinding::bind) 27 | private val vm by viewModels() 28 | private val args: DetailFragmentArgs by navArgs() 29 | private val reviewAdapter by lazy { ReviewAdapter() } 30 | 31 | 32 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 33 | super.onViewCreated(view, savedInstanceState) 34 | 35 | viewLifecycleOwner.lifecycleScope.launch { 36 | repeatOnLifecycle(Lifecycle.State.STARTED) { 37 | vm.getGameDetailById(args.gameId) 38 | } 39 | } 40 | 41 | detailUiState() 42 | goBackStack() 43 | 44 | } 45 | 46 | private fun goBackStack() = with(binding) { 47 | detailToolbar.setNavigationOnClickListener { 48 | findNavController().popBackStack() 49 | } 50 | } 51 | 52 | private fun handleDetailUiState(response: DetailUiState) = with(binding) { 53 | when (response) { 54 | is DetailUiState.Loading -> { 55 | shimmerDetailContainer.startShimmer() 56 | shimmerDetailContainer.visibility = View.VISIBLE 57 | nestedScrollView.isNestedScrollingEnabled = false 58 | } 59 | 60 | is DetailUiState.Error -> { 61 | shimmerDetailContainer.stopShimmer() 62 | shimmerDetailContainer.visibility = View.GONE 63 | nestedScrollView.isNestedScrollingEnabled = false 64 | } 65 | 66 | is DetailUiState.Success -> { 67 | nestedScrollView.isNestedScrollingEnabled = true 68 | shimmerDetailContainer.stopShimmer() 69 | shimmerDetailContainer.visibility = View.GONE 70 | setDetailData(response.data) 71 | } 72 | } 73 | } 74 | 75 | private fun setDetailData(detailResult: GameDetailUi) = with(binding) { 76 | shimmerDetailContainer.stopShimmer() 77 | shimmerDetailContainer.visibility = View.GONE 78 | detailImageView.load(detailResult.thumbnail) { 79 | crossfade(true) 80 | placeholder(R.drawable.game_placeholder) 81 | } 82 | 83 | detailDesc.makeCollapsible(3, Int.MAX_VALUE, expandMoreDrawable) 84 | 85 | detailTitle.text = detailResult.title 86 | detailGenre.text = detailResult.genre 87 | detailPlatform.text = detailResult.platform 88 | detailReleaseDate.text = 89 | detailResult.releaseDate?.extractYearFromDateString() 90 | detailDesc.text = detailResult.description 91 | 92 | if (detailResult.minimumSystemRequirements == null) { 93 | systemReqConstraintLayout.visibility = View.GONE 94 | viewLineAbout.visibility = View.GONE 95 | viewLineSystemReq.visibility = View.GONE 96 | } 97 | systemReqOS.text = detailResult.minimumSystemRequirements?.os 98 | systemReqCPU.text = 99 | detailResult.minimumSystemRequirements?.processor ?: "" 100 | systemReqRAM.text = detailResult.minimumSystemRequirements?.memory ?: "" 101 | systemReqStorage.text = 102 | detailResult.minimumSystemRequirements?.storage ?: "" 103 | systemReqGraphics.text = 104 | detailResult.minimumSystemRequirements?.graphics ?: "" 105 | 106 | screenshotRecyclerView.adapter = reviewAdapter 107 | reviewAdapter.submitList(detailResult.screenshots ?: emptyList()) 108 | 109 | if (detailResult.screenshots.isNullOrEmpty()) { 110 | screenshotTitle.visibility = View.GONE 111 | } 112 | 113 | nestedScrollView.setOnScrollChangeListener(NestedScrollView.OnScrollChangeListener { _, _, scrollY, _, _ -> 114 | 115 | if (scrollY >= detailTitle.top + detailTitle.height) { 116 | detailToolbar.title = detailTitle.text 117 | } else { 118 | detailToolbar.title = "" 119 | } 120 | }) 121 | } 122 | 123 | private fun detailUiState() { 124 | viewLifecycleOwner.lifecycleScope.launch { 125 | vm.detailData.collect { response -> 126 | handleDetailUiState(response) 127 | } 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /app/src/main/java/com/hamzaazman/kotlinfreetoplay/ui/detail/DetailUiState.kt: -------------------------------------------------------------------------------- 1 | package com.hamzaazman.kotlinfreetoplay.ui.detail 2 | 3 | import com.hamzaazman.kotlinfreetoplay.domain.model.GameDetailUi 4 | 5 | sealed class DetailUiState { 6 | object Loading : DetailUiState() 7 | data class Success(val data: GameDetailUi) : DetailUiState() 8 | data class Error(val message: String) : DetailUiState() 9 | } 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/hamzaazman/kotlinfreetoplay/ui/detail/DetailViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.hamzaazman.kotlinfreetoplay.ui.detail 2 | 3 | import android.util.Log 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import com.hamzaazman.kotlinfreetoplay.common.NetworkResource 7 | import com.hamzaazman.kotlinfreetoplay.data.repository.GameRepositoryImpl 8 | import dagger.hilt.android.lifecycle.HiltViewModel 9 | import kotlinx.coroutines.Dispatchers 10 | import kotlinx.coroutines.flow.MutableStateFlow 11 | import kotlinx.coroutines.flow.StateFlow 12 | import kotlinx.coroutines.flow.asStateFlow 13 | import kotlinx.coroutines.flow.collect 14 | import kotlinx.coroutines.flow.flowOn 15 | import kotlinx.coroutines.flow.onEach 16 | import kotlinx.coroutines.launch 17 | import javax.inject.Inject 18 | 19 | @HiltViewModel 20 | class DetailViewModel @Inject constructor( 21 | private val gameRepository: GameRepositoryImpl, 22 | ) : ViewModel() { 23 | 24 | private val _detailData: MutableStateFlow = 25 | MutableStateFlow(DetailUiState.Loading) 26 | val detailData: StateFlow get() = _detailData.asStateFlow() 27 | 28 | suspend fun getGameDetailById(id: Int) = viewModelScope.launch { 29 | gameRepository.getGameDetailById(id).onEach { response -> 30 | when (response) { 31 | is NetworkResource.Loading -> { 32 | _detailData.value = DetailUiState.Loading 33 | } 34 | 35 | is NetworkResource.Error -> { 36 | _detailData.value = DetailUiState.Error(response.throwable.toString()) 37 | } 38 | 39 | is NetworkResource.Success -> { 40 | _detailData.value = DetailUiState.Success(data = response.data) 41 | } 42 | } 43 | }.flowOn(Dispatchers.IO).collect() 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hamzaazman/kotlinfreetoplay/ui/detail/ReviewAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.hamzaazman.kotlinfreetoplay.ui.detail 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import androidx.recyclerview.widget.DiffUtil 6 | import androidx.recyclerview.widget.ListAdapter 7 | import androidx.recyclerview.widget.RecyclerView 8 | import coil.load 9 | import com.hamzaazman.kotlinfreetoplay.R 10 | import com.hamzaazman.kotlinfreetoplay.data.dto.Screenshot 11 | import com.hamzaazman.kotlinfreetoplay.databinding.ReviewRowItemBinding 12 | 13 | 14 | class ReviewAdapter : ListAdapter(DiffCallback()) { 15 | 16 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 17 | val binding = 18 | ReviewRowItemBinding.inflate( 19 | LayoutInflater.from(parent.context), 20 | parent, 21 | false 22 | ) 23 | return ViewHolder(binding) 24 | } 25 | 26 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 27 | val currentItem = getItem(position) 28 | holder.bind(currentItem) 29 | } 30 | 31 | inner class ViewHolder(private val binding: ReviewRowItemBinding) : 32 | RecyclerView.ViewHolder(binding.root) { 33 | 34 | fun bind(screenshot: Screenshot) = with(binding) { 35 | reviewImageView.load(screenshot.image) { 36 | crossfade(true) 37 | placeholder(R.drawable.game_placeholder) 38 | } 39 | } 40 | } 41 | 42 | class DiffCallback : DiffUtil.ItemCallback() { 43 | override fun areItemsTheSame(oldItem: Screenshot, newItem: Screenshot) = 44 | oldItem.id == newItem.id 45 | 46 | override fun areContentsTheSame(oldItem: Screenshot, newItem: Screenshot) = 47 | oldItem == newItem 48 | } 49 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hamzaazman/kotlinfreetoplay/ui/home/GamePlatform.kt: -------------------------------------------------------------------------------- 1 | package com.hamzaazman.kotlinfreetoplay.ui.home 2 | 3 | enum class GamePlatform { 4 | WINDOWS, 5 | BROWSER 6 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hamzaazman/kotlinfreetoplay/ui/home/HomeAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.hamzaazman.kotlinfreetoplay.ui.home 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import androidx.recyclerview.widget.DiffUtil 6 | import androidx.recyclerview.widget.ListAdapter 7 | import androidx.recyclerview.widget.RecyclerView 8 | import coil.load 9 | import com.hamzaazman.kotlinfreetoplay.R 10 | import com.hamzaazman.kotlinfreetoplay.databinding.GameRowItemBinding 11 | import com.hamzaazman.kotlinfreetoplay.domain.model.GameUi 12 | 13 | class HomeAdapter( 14 | private val onItemClick: (item: GameUi) -> Unit 15 | ) : ListAdapter(DiffCallback()) { 16 | 17 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 18 | val binding = 19 | GameRowItemBinding.inflate( 20 | LayoutInflater.from(parent.context), 21 | parent, 22 | false 23 | ) 24 | return ViewHolder(binding) 25 | } 26 | 27 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 28 | val currentItem = getItem(position) 29 | holder.bind(currentItem) 30 | } 31 | 32 | inner class ViewHolder(private val binding: GameRowItemBinding) : 33 | RecyclerView.ViewHolder(binding.root) { 34 | fun bind(gameUi: GameUi) = with(binding) { 35 | gameTitle.text = gameUi.title 36 | gameDescription.text = gameUi.short_description 37 | gameGenre.text = gameUi.genre 38 | 39 | gameImage.load(gameUi.thumbnail) { 40 | crossfade(true) 41 | placeholder(R.drawable.game_placeholder) 42 | } 43 | 44 | if (gameUi.platform.contains("Windows")) { 45 | gamePlatform.setImageResource(R.drawable.windows) 46 | } 47 | 48 | if (gameUi.platform.contains("Browser")) { 49 | gamePlatform.setImageResource(R.drawable.browser) 50 | } 51 | binding.root.setOnClickListener { onItemClick(gameUi) } 52 | } 53 | } 54 | 55 | class DiffCallback : DiffUtil.ItemCallback() { 56 | override fun areItemsTheSame(oldItem: GameUi, newItem: GameUi) = 57 | oldItem.id == newItem.id 58 | 59 | override fun areContentsTheSame(oldItem: GameUi, newItem: GameUi) = 60 | oldItem == newItem 61 | } 62 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hamzaazman/kotlinfreetoplay/ui/home/HomeFragment.kt: -------------------------------------------------------------------------------- 1 | package com.hamzaazman.kotlinfreetoplay.ui.home 2 | 3 | import android.os.Bundle 4 | import android.transition.ChangeBounds 5 | import android.transition.TransitionManager 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import androidx.fragment.app.Fragment 9 | import androidx.fragment.app.viewModels 10 | import androidx.lifecycle.lifecycleScope 11 | import androidx.navigation.fragment.findNavController 12 | import androidx.recyclerview.widget.DividerItemDecoration 13 | import androidx.recyclerview.widget.LinearLayoutManager 14 | import com.google.android.material.chip.Chip 15 | import com.google.android.material.chip.ChipGroup 16 | import com.hamzaazman.kotlinfreetoplay.HorizontalItemDecoration 17 | import com.hamzaazman.kotlinfreetoplay.R 18 | import com.hamzaazman.kotlinfreetoplay.VerticalItemDecoration 19 | import com.hamzaazman.kotlinfreetoplay.capitalizeFirstLetter 20 | import com.hamzaazman.kotlinfreetoplay.common.viewBinding 21 | import com.hamzaazman.kotlinfreetoplay.databinding.FragmentHomeBinding 22 | import dagger.hilt.android.AndroidEntryPoint 23 | import kotlinx.coroutines.flow.collectLatest 24 | import kotlinx.coroutines.launch 25 | 26 | 27 | @AndroidEntryPoint 28 | class HomeFragment : Fragment(R.layout.fragment_home) { 29 | private val binding by viewBinding(FragmentHomeBinding::bind) 30 | private val vm by viewModels() 31 | private val homeAdapter by lazy { 32 | HomeAdapter { gameUi -> 33 | val action = 34 | HomeFragmentDirections.actionNavigationHomeToDetailFragment(gameId = gameUi.id) 35 | findNavController().navigate(action) 36 | } 37 | } 38 | 39 | private var checkedCategory = "" 40 | private var checkedCategoryId = 0 41 | 42 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 43 | super.onViewCreated(view, savedInstanceState) 44 | setupRv() 45 | selectCategory() 46 | uiState() 47 | } 48 | 49 | private fun selectCategory() = with(binding) { 50 | if (checkedCategory.isEmpty()) { 51 | vm.getAllGame() 52 | } 53 | lifecycleScope.launch { 54 | vm.getCategoryAndId.collect { categoryType -> 55 | clearChip.visibility = 56 | if (categoryType.checkedCategoryId == 0) View.GONE else View.VISIBLE 57 | 58 | checkedCategory = categoryType.checkedCategory.apply { 59 | if (!this.contains("clear filter")) { 60 | toolbarTextView.text = this.capitalizeFirstLetter() 61 | } 62 | } 63 | checkedCategoryId = categoryType.checkedCategoryId 64 | updateChip(checkedCategoryId, categoryChipGroup) 65 | clearChip.visibility = if (checkedCategory == "home") View.GONE else View.VISIBLE 66 | } 67 | } 68 | categoryChipGroup.setOnCheckedStateChangeListener { group, checkedIds -> 69 | checkedIds.forEach { 70 | checkedCategory = group.findViewById(it).text.toString().lowercase() 71 | lifecycleScope.launch { 72 | vm.saveCategoryAndId(category = checkedCategory, categoryId = it) 73 | if (!checkedCategory.contains("clear filter")) { 74 | vm.getGameByCategory(checkedCategory) 75 | } 76 | } 77 | 78 | val transition = ChangeBounds() 79 | transition.duration = 200 80 | TransitionManager.beginDelayedTransition( 81 | categoryChipGroup.parent as ViewGroup, 82 | transition 83 | ) 84 | } 85 | } 86 | 87 | clearChip.setOnClickListener { 88 | lifecycleScope.launch { 89 | vm.clearCategoryFilter() 90 | vm.getAllGame() 91 | clearChip.visibility = View.GONE 92 | } 93 | } 94 | } 95 | 96 | 97 | private fun updateChip(chipId: Int, chipGroup: ChipGroup) { 98 | if (chipId != 0) { 99 | try { 100 | chipGroup.findViewById(chipId).isChecked = true 101 | } catch (e: Exception) { 102 | binding.gameError.text = e.message 103 | } 104 | } 105 | } 106 | 107 | private fun setupRv() = with(binding) { 108 | gameRecycler.apply { 109 | adapter = homeAdapter 110 | setHasFixedSize(false) 111 | addItemDecoration(VerticalItemDecoration(26)) 112 | addItemDecoration(HorizontalItemDecoration(42)) 113 | addItemDecoration( 114 | DividerItemDecoration( 115 | requireContext(), LinearLayoutManager.VERTICAL 116 | ) 117 | ) 118 | } 119 | } 120 | 121 | private fun uiState() = with(binding) { 122 | lifecycleScope.launch { 123 | vm.gameList.collectLatest { state -> 124 | when (state) { 125 | is HomeUiState.Success -> { 126 | gameError.visibility = View.GONE 127 | gameRecycler.visibility = View.VISIBLE 128 | shimmerViewContainer.apply { 129 | stopShimmer() 130 | visibility = View.GONE 131 | } 132 | homeAdapter.submitList(state.data) 133 | } 134 | 135 | is HomeUiState.Error -> { 136 | gameRecycler.visibility = View.GONE 137 | gameError.apply { 138 | visibility = View.VISIBLE 139 | text = state.message 140 | } 141 | shimmerViewContainer.apply { 142 | stopShimmer() 143 | visibility = View.GONE 144 | } 145 | } 146 | 147 | is HomeUiState.Loading -> { 148 | gameError.visibility = View.GONE 149 | gameRecycler.visibility = View.GONE 150 | shimmerViewContainer.apply { 151 | startShimmer() 152 | visibility = View.VISIBLE 153 | } 154 | } 155 | } 156 | } 157 | } 158 | } 159 | 160 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hamzaazman/kotlinfreetoplay/ui/home/HomeUiState.kt: -------------------------------------------------------------------------------- 1 | package com.hamzaazman.kotlinfreetoplay.ui.home 2 | 3 | import com.hamzaazman.kotlinfreetoplay.domain.model.GameUi 4 | 5 | sealed class HomeUiState { 6 | object Loading : HomeUiState() 7 | data class Success(val data: List) : HomeUiState() 8 | data class Error(val message: String) : HomeUiState() 9 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hamzaazman/kotlinfreetoplay/ui/home/HomeViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.hamzaazman.kotlinfreetoplay.ui.home 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import com.hamzaazman.kotlinfreetoplay.common.NetworkResource 6 | import com.hamzaazman.kotlinfreetoplay.data.datastore.CategoryType 7 | import com.hamzaazman.kotlinfreetoplay.data.datastore.DataStoreRepositoryImpl 8 | import com.hamzaazman.kotlinfreetoplay.data.repository.GameRepositoryImpl 9 | import dagger.hilt.android.lifecycle.HiltViewModel 10 | import kotlinx.coroutines.Dispatchers 11 | import kotlinx.coroutines.flow.Flow 12 | import kotlinx.coroutines.flow.MutableStateFlow 13 | import kotlinx.coroutines.flow.StateFlow 14 | import kotlinx.coroutines.flow.asStateFlow 15 | import kotlinx.coroutines.flow.launchIn 16 | import kotlinx.coroutines.flow.onEach 17 | import kotlinx.coroutines.launch 18 | import javax.inject.Inject 19 | 20 | @HiltViewModel 21 | class HomeViewModel @Inject constructor( 22 | private val gameRepository: GameRepositoryImpl, 23 | private val dataStoreRepository: DataStoreRepositoryImpl 24 | ) : ViewModel() { 25 | 26 | private val _gameList: MutableStateFlow = MutableStateFlow(HomeUiState.Loading) 27 | val gameList: StateFlow get() = _gameList.asStateFlow() 28 | 29 | val getCategoryAndId: Flow = dataStoreRepository.getCategoryAndId 30 | suspend fun clearCategoryFilter() = dataStoreRepository.clearCategory() 31 | 32 | fun saveCategoryAndId(category: String, categoryId: Int) = 33 | viewModelScope.launch(Dispatchers.IO) { 34 | dataStoreRepository.saveCategoryAndId(category = category, categoryId = categoryId) 35 | } 36 | 37 | fun getAllGame() = viewModelScope.launch(Dispatchers.IO) { 38 | gameRepository.getAllGame().onEach { response -> 39 | when (response) { 40 | is NetworkResource.Loading -> { 41 | _gameList.value = HomeUiState.Loading 42 | } 43 | 44 | is NetworkResource.Error -> { 45 | _gameList.value = HomeUiState.Error(response.throwable.toString()) 46 | } 47 | 48 | is NetworkResource.Success -> { 49 | _gameList.value = HomeUiState.Success(data = response.data) 50 | } 51 | } 52 | }.launchIn(viewModelScope) 53 | } 54 | 55 | suspend fun getGameByCategory(category: String) { 56 | gameRepository.getGameByCategory(category).onEach { response -> 57 | when (response) { 58 | is NetworkResource.Loading -> { 59 | _gameList.value = HomeUiState.Loading 60 | } 61 | 62 | is NetworkResource.Error -> { 63 | _gameList.value = HomeUiState.Error(response.throwable.toString()) 64 | } 65 | 66 | is NetworkResource.Success -> { 67 | _gameList.value = HomeUiState.Success(data = response.data) 68 | } 69 | } 70 | }.launchIn(viewModelScope) 71 | } 72 | 73 | } -------------------------------------------------------------------------------- /app/src/main/res/anim/recyclerview_alpha.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/color/chip_select_state.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/color/chip_unselect_state.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_filter_list.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/browser.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/category_shape.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/expand_more_dark.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/expand_more_white.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/free_shape.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/game_image_shape.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/game_placeholder.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 18 | 24 | 30 | 33 | 36 | 39 | 42 | 45 | 46 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/gradient_image_shape.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_back.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_dashboard_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_home_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_notifications_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/shimmer_image_shape.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/windows.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/layout/filter_drawer_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 16 | 17 | 18 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_detail.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 16 | 17 | 23 | 24 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 44 | 45 | 49 | 50 | 58 | 59 | 63 | 64 | 65 | 66 | 69 | 70 | 79 | 80 | 92 | 93 | 106 | 107 | 121 | 122 | 136 | 137 | 146 | 147 | 148 | 164 | 165 | 173 | 174 | 184 | 185 | 186 | 193 | 194 | 205 | 206 | 216 | 217 | 230 | 231 | 241 | 242 | 254 | 255 | 265 | 266 | 278 | 279 | 289 | 290 | 302 | 303 | 313 | 314 | 326 | 327 | 328 | 338 | 339 | 350 | 351 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_home.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 21 | 22 | 29 | 30 | 39 | 40 | 41 | 42 | 50 | 51 | 60 | 61 | 67 | 68 | 75 | 76 | 83 | 84 | 91 | 92 | 99 | 100 | 107 | 108 | 115 | 116 | 123 | 124 | 131 | 132 | 139 | 140 | 147 | 148 | 155 | 156 | 163 | 164 | 171 | 172 | 173 | 174 | 175 | 181 | 182 | 192 | 193 | 201 | 202 | 206 | 207 | 210 | 211 | 212 | 213 | 216 | 217 | 218 | 219 | 222 | 223 | 224 | 225 | 228 | 229 | 230 | 231 | 234 | 235 | 236 | 237 | 240 | 241 | 242 | 243 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 262 | 263 | 264 | -------------------------------------------------------------------------------- /app/src/main/res/layout/game_row_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 18 | 19 | 31 | 32 | 43 | 44 | 54 | 55 | 66 | 67 | 77 | 78 | -------------------------------------------------------------------------------- /app/src/main/res/layout/review_row_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/layout/shimmer_detail_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 | 20 | 21 | 26 | 27 | 33 | 34 | 41 | 42 | 49 | 50 | 51 | 52 | 58 | 59 | 65 | 66 | 70 | 71 | 81 | 82 | 91 | 92 | 103 | 104 | 113 | 114 | 124 | 125 | 134 | 135 | 146 | 147 | 156 | 157 | 168 | 169 | 178 | 179 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 203 | 204 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | -------------------------------------------------------------------------------- /app/src/main/res/layout/shimmer_game_row_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 20 | 21 | 34 | 35 | 47 | 48 | 58 | 59 | 70 | 71 | 82 | 83 | -------------------------------------------------------------------------------- /app/src/main/res/layout/shimmer_review_row_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/menu/bottom_nav_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/menu/filter_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamzaazman/FreeToGame/d484dee71ca7eba00a09a072b6a831032b363aa4/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamzaazman/FreeToGame/d484dee71ca7eba00a09a072b6a831032b363aa4/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamzaazman/FreeToGame/d484dee71ca7eba00a09a072b6a831032b363aa4/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamzaazman/FreeToGame/d484dee71ca7eba00a09a072b6a831032b363aa4/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamzaazman/FreeToGame/d484dee71ca7eba00a09a072b6a831032b363aa4/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamzaazman/FreeToGame/d484dee71ca7eba00a09a072b6a831032b363aa4/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamzaazman/FreeToGame/d484dee71ca7eba00a09a072b6a831032b363aa4/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamzaazman/FreeToGame/d484dee71ca7eba00a09a072b6a831032b363aa4/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamzaazman/FreeToGame/d484dee71ca7eba00a09a072b6a831032b363aa4/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamzaazman/FreeToGame/d484dee71ca7eba00a09a072b6a831032b363aa4/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/navigation/mobile_navigation.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 16 | 17 | 22 | 25 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20 | 21 | 24 | 25 | 28 | 29 | 40 | 41 | 52 | 53 | 56 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | 11 | #272B30 12 | #007BFF 13 | #DEDEDE 14 | #ECECEC 15 | 16 | #212121 17 | #FFFFFF 18 | 19 | #FFFFFF 20 | #000000 21 | 22 | #0B8E27 23 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | KotlinFreeToPlay 3 | Home 4 | Detail 5 | Call of Duty: Warzone is both a standalone free-to-play battle royale and modes accessible via Call of Duty: Modern Warfare. Warzone features two modes — the general 150-player battle royle, and “Plunder”. The latter mode is described as a “race to deposit the most Cash”. In both modes players can both earn and loot cash to be used when purchasing in-match equipment, field upgrades, and more. Both cash and XP are earned in a variety of ways, including completing contracts.\r\n\r\nAn interesting feature of the game is one that allows players who have been killed in a match to rejoin it by winning a 1v1 match against other felled players in the Gulag.\r\n\r\nOf course, being a battle royale, the game does offer a battle pass. The pass offers players new weapons, playable characters, Call of Duty points, blueprints, and more. Players can also earn plenty of new items by completing objectives offered with the pass. 6 | 7 | Mmorpg 8 | Shooter 9 | Moba 10 | Anime 11 | Battle Royale 12 | Strategy 13 | Fantasy 14 | Sci-Fi 15 | Card Games 16 | Racing 17 | Fighting 18 | Social 19 | Sports 20 | 21 | 22 | lorem ipsum dolor sit amet 23 | lorem ipsum dolor sit amet 24 | lorem ipsum dolor sit amet 25 | lorem ipsum dolor sit amet 26 | lorem ipsum dolor sit amet 27 | lorem ipsum dolor sit amet 28 | lorem ipsum dolor sit amet 29 | lorem ipsum dolor sit amet 30 | lorem ipsum dolor sit amet 31 | lorem ipsum dolor sit amet 32 | lorem ipsum dolor sit amet 33 | lorem ipsum dolor sit amet 34 | lorem ipsum dolor sit amet 35 | 36 | MMORPG 37 | Shooter 38 | MOBA 39 | Anime 40 | Battle-Royale 41 | Strategy 42 | Fantasy 43 | Sci-Fi 44 | Card-Games 45 | Racing 46 | Fighting 47 | Social 48 | Sports 49 | Clear Filter 50 | Screenshots 51 | System Requirements 52 | OS: 53 | CPU: 54 | RAM: 55 | Storage: 56 | About 57 | Graphic: 58 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 19 | 20 | 23 | 24 | 36 | 37 | 50 | 51 | 52 | 55 | -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/xml/datastore_preference.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/test/java/com/hamzaazman/kotlinfreetoplay/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.hamzaazman.kotlinfreetoplay 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 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | dependencies { 3 | def nav_version = "2.5.3" 4 | classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version" 5 | } 6 | } 7 | plugins { 8 | id 'com.android.application' version '8.0.0' apply false 9 | id 'com.android.library' version '8.0.0' apply false 10 | id 'org.jetbrains.kotlin.android' version '1.8.21' apply false 11 | id 'com.google.dagger.hilt.android' version '2.46.1' apply false 12 | } -------------------------------------------------------------------------------- /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 24 | org.gradle.unsafe.configuration-cache=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamzaazman/FreeToGame/d484dee71ca7eba00a09a072b6a831032b363aa4/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun Apr 30 19:44:29 TRT 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /screenshots/detail_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamzaazman/FreeToGame/d484dee71ca7eba00a09a072b6a831032b363aa4/screenshots/detail_dark.png -------------------------------------------------------------------------------- /screenshots/detail_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamzaazman/FreeToGame/d484dee71ca7eba00a09a072b6a831032b363aa4/screenshots/detail_light.png -------------------------------------------------------------------------------- /screenshots/freetogame_preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamzaazman/FreeToGame/d484dee71ca7eba00a09a072b6a831032b363aa4/screenshots/freetogame_preview.png -------------------------------------------------------------------------------- /screenshots/home_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamzaazman/FreeToGame/d484dee71ca7eba00a09a072b6a831032b363aa4/screenshots/home_dark.png -------------------------------------------------------------------------------- /screenshots/home_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamzaazman/FreeToGame/d484dee71ca7eba00a09a072b6a831032b363aa4/screenshots/home_light.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | gradlePluginPortal() 6 | } 7 | } 8 | dependencyResolutionManagement { 9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 10 | repositories { 11 | google() 12 | mavenCentral() 13 | } 14 | } 15 | rootProject.name = "KotlinFreeToPlay" 16 | include ':app' 17 | --------------------------------------------------------------------------------