├── .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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | xmlns:android
18 |
19 | ^$
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | xmlns:.*
29 |
30 | ^$
31 |
32 |
33 | BY_NAME
34 |
35 |
36 |
37 |
38 |
39 |
40 | .*:id
41 |
42 | http://schemas.android.com/apk/res/android
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | .*:name
52 |
53 | http://schemas.android.com/apk/res/android
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | name
63 |
64 | ^$
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 | style
74 |
75 | ^$
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 | .*
85 |
86 | ^$
87 |
88 |
89 | BY_NAME
90 |
91 |
92 |
93 |
94 |
95 |
96 | .*
97 |
98 | http://schemas.android.com/apk/res/android
99 |
100 |
101 | ANDROID_ATTRIBUTE_ORDER
102 |
103 |
104 |
105 |
106 |
107 |
108 | .*
109 |
110 | .*
111 |
112 |
113 | BY_NAME
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
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 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/render.experimental.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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://tooomm.github.io/github-release-stats/?username=hamzaazman&repository=KotlinFreeToGame)
4 | [](https://hits.seeyoufarm.com)
5 | [](https://github.com/hamzaazman/KotlinFreeToGame/releases)
6 | -839192?logo=android&logoColor=white)
7 | -566573?logo=android&logoColor=white)
8 | 
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 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/filter_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------