├── .idea
├── .name
├── .gitignore
├── vcs.xml
├── compiler.xml
├── kotlinc.xml
├── migrations.xml
├── misc.xml
├── gradle.xml
├── appInsightsSettings.xml
├── deploymentTargetSelector.xml
└── inspectionProfiles
│ └── Project_Default.xml
├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── values
│ │ │ │ ├── strings.xml
│ │ │ │ ├── themes.xml
│ │ │ │ └── colors.xml
│ │ │ ├── font
│ │ │ │ ├── poppins_bold.ttf
│ │ │ │ ├── poppins_light.ttf
│ │ │ │ └── poppins_senibold.ttf
│ │ │ ├── mipmap-hdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ ├── ic_launcher_round.webp
│ │ │ │ └── ic_launcher_foreground.webp
│ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ ├── ic_launcher_round.webp
│ │ │ │ └── ic_launcher_foreground.webp
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ ├── ic_launcher_round.webp
│ │ │ │ └── ic_launcher_foreground.webp
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ ├── ic_launcher_round.webp
│ │ │ │ └── ic_launcher_foreground.webp
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ ├── ic_launcher_round.webp
│ │ │ │ └── ic_launcher_foreground.webp
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ ├── xml
│ │ │ │ ├── backup_rules.xml
│ │ │ │ └── data_extraction_rules.xml
│ │ │ ├── drawable
│ │ │ │ ├── ic_launcher_foreground.xml
│ │ │ │ └── ic_launcher_background.xml
│ │ │ └── raw
│ │ │ │ ├── anim3.json
│ │ │ │ └── anim2.json
│ │ ├── ic_launcher-playstore.png
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── example
│ │ │ │ └── investiq
│ │ │ │ ├── constant
│ │ │ │ └── Constant.kt
│ │ │ │ ├── InvestIqApplication.kt
│ │ │ │ ├── domain
│ │ │ │ ├── model
│ │ │ │ │ ├── IntraDayInfo.kt
│ │ │ │ │ ├── CompanyDetail.kt
│ │ │ │ │ ├── CompanyFavItem.kt
│ │ │ │ │ ├── CompanyQuote.kt
│ │ │ │ │ └── CompanyItem.kt
│ │ │ │ └── respository
│ │ │ │ │ ├── FavCompanyRepository.kt
│ │ │ │ │ └── StockRepository.kt
│ │ │ │ ├── presentation
│ │ │ │ ├── screens
│ │ │ │ │ └── Screens.kt
│ │ │ │ ├── company_favorites
│ │ │ │ │ ├── CompanyFavoriteState.kt
│ │ │ │ │ ├── CompanyFavoriteEvent.kt
│ │ │ │ │ ├── CompanyFavoriteViewModel.kt
│ │ │ │ │ ├── CompanyFavouriteScreen.kt
│ │ │ │ │ └── CompanyFavoriteItem.kt
│ │ │ │ ├── company_list
│ │ │ │ │ ├── CompanyListingEvent.kt
│ │ │ │ │ ├── CompanyListingState.kt
│ │ │ │ │ ├── CompanyListingViewmodel.kt
│ │ │ │ │ ├── CompanyItem.kt
│ │ │ │ │ └── CompanyListScreen.kt
│ │ │ │ ├── company_info
│ │ │ │ │ ├── CompanyInfoState.kt
│ │ │ │ │ ├── CompanyInfoViewModel.kt
│ │ │ │ │ ├── StockChart.kt
│ │ │ │ │ └── CompanyInfoScreen.kt
│ │ │ │ └── bottomNav
│ │ │ │ │ └── BottomBarTab.kt
│ │ │ │ ├── data
│ │ │ │ ├── remote
│ │ │ │ │ ├── dto
│ │ │ │ │ │ ├── CompanyItemDto.kt
│ │ │ │ │ │ ├── IntraDayDto.kt
│ │ │ │ │ │ ├── CompanyQuoteDto.kt
│ │ │ │ │ │ └── CompanyDetailDto.kt
│ │ │ │ │ └── StockApi.kt
│ │ │ │ ├── local
│ │ │ │ │ ├── StockDatabase.kt
│ │ │ │ │ ├── CompanyItemEntity.kt
│ │ │ │ │ ├── FavCompanyEntity.kt
│ │ │ │ │ └── StockDao.kt
│ │ │ │ ├── repository
│ │ │ │ │ ├── FavCompanyRepositoryImpl.kt
│ │ │ │ │ └── StockRepositoryImpl.kt
│ │ │ │ └── mappers
│ │ │ │ │ └── CompanyMappers.kt
│ │ │ │ ├── util
│ │ │ │ └── Resource.kt
│ │ │ │ ├── ui
│ │ │ │ └── theme
│ │ │ │ │ ├── Color.kt
│ │ │ │ │ ├── Type.kt
│ │ │ │ │ └── Theme.kt
│ │ │ │ ├── di
│ │ │ │ ├── RepositoryModule.kt
│ │ │ │ └── AppModule.kt
│ │ │ │ └── MainActivity.kt
│ │ └── AndroidManifest.xml
│ ├── test
│ │ └── java
│ │ │ └── com
│ │ │ └── example
│ │ │ └── investiq
│ │ │ └── ExampleUnitTest.kt
│ └── androidTest
│ │ └── java
│ │ └── com
│ │ └── example
│ │ └── investiq
│ │ └── ExampleInstrumentedTest.kt
├── proguard-rules.pro
└── build.gradle.kts
├── images
├── banner.png
└── InvestIq.png
├── gradle
├── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
└── libs.versions.toml
├── .gitignore
├── settings.gradle.kts
├── CONTRIBUTING.md
├── gradle.properties
├── README.md
├── gradlew.bat
├── CODE_OF_CONDUCT.md
└── gradlew
/.idea/.name:
--------------------------------------------------------------------------------
1 | InvestIQ
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/images/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shalenMathew/InvestIq-AndroidApp/HEAD/images/banner.png
--------------------------------------------------------------------------------
/images/InvestIq.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shalenMathew/InvestIq-AndroidApp/HEAD/images/InvestIq.png
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | InvestIQ
3 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shalenMathew/InvestIq-AndroidApp/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shalenMathew/InvestIq-AndroidApp/HEAD/app/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/app/src/main/res/font/poppins_bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shalenMathew/InvestIq-AndroidApp/HEAD/app/src/main/res/font/poppins_bold.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/poppins_light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shalenMathew/InvestIq-AndroidApp/HEAD/app/src/main/res/font/poppins_light.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/poppins_senibold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shalenMathew/InvestIq-AndroidApp/HEAD/app/src/main/res/font/poppins_senibold.ttf
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shalenMathew/InvestIq-AndroidApp/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shalenMathew/InvestIq-AndroidApp/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shalenMathew/InvestIq-AndroidApp/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shalenMathew/InvestIq-AndroidApp/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shalenMathew/InvestIq-AndroidApp/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shalenMathew/InvestIq-AndroidApp/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shalenMathew/InvestIq-AndroidApp/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shalenMathew/InvestIq-AndroidApp/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shalenMathew/InvestIq-AndroidApp/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shalenMathew/InvestIq-AndroidApp/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shalenMathew/InvestIq-AndroidApp/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shalenMathew/InvestIq-AndroidApp/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shalenMathew/InvestIq-AndroidApp/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shalenMathew/InvestIq-AndroidApp/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shalenMathew/InvestIq-AndroidApp/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/investiq/constant/Constant.kt:
--------------------------------------------------------------------------------
1 | package com.example.investiq.constant
2 |
3 | object Constant {
4 | const val FMP_BASE_URL= "https://financialmodelingprep.com"
5 |
6 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/investiq/InvestIqApplication.kt:
--------------------------------------------------------------------------------
1 | package com.example.investiq
2 |
3 | import android.app.Application
4 | import dagger.hilt.android.HiltAndroidApp
5 |
6 | @HiltAndroidApp
7 | class InvestIqApplication: Application() {
8 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/investiq/domain/model/IntraDayInfo.kt:
--------------------------------------------------------------------------------
1 | package com.example.investiq.domain.model
2 |
3 | import java.time.LocalDateTime
4 |
5 | data class IntraDayInfo(
6 | val close: Double,
7 | val date: LocalDateTime
8 | )
9 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/investiq/domain/model/CompanyDetail.kt:
--------------------------------------------------------------------------------
1 | package com.example.investiq.domain.model
2 |
3 | data class CompanyDetail(
4 | val symbol: String,
5 | val companyName: String,
6 | val image: String,
7 | val dcf: Double
8 | )
9 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/investiq/presentation/screens/Screens.kt:
--------------------------------------------------------------------------------
1 | package com.example.investiq.presentation.screens
2 |
3 | sealed class Screen (val route:String){
4 | object HomeScreen:Screen("HomeScreen")
5 | object FavoritesScreen:Screen("FavoritesScreen")
6 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/investiq/domain/model/CompanyFavItem.kt:
--------------------------------------------------------------------------------
1 | package com.example.investiq.domain.model
2 |
3 | data class CompanyFavItem(
4 | val symbol:String,
5 | val name:String,
6 | val price:String,
7 | val exchangeShortName:String,
8 | val id:Int?=null
9 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/example/investiq/data/remote/dto/CompanyItemDto.kt:
--------------------------------------------------------------------------------
1 | package com.example.investiq.data.remote.dto
2 |
3 | data class CompanyItemDto(
4 | val symbol:String?,
5 | val name:String?,
6 | val price:String?,
7 | val exchangeShortName:String?,
8 | )
9 |
10 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Wed Jun 05 13:40:08 IST 2024
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | local.properties
16 |
--------------------------------------------------------------------------------
/.idea/migrations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/investiq/data/remote/dto/IntraDayDto.kt:
--------------------------------------------------------------------------------
1 | package com.example.investiq.data.remote.dto
2 |
3 | data class IntraDayDto(
4 | val close: Double?,
5 | val date: String?,
6 | val high: Double?,
7 | val low: Double?,
8 | val open: Double?,
9 | val volume: Int?
10 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/example/investiq/domain/model/CompanyQuote.kt:
--------------------------------------------------------------------------------
1 | package com.example.investiq.domain.model
2 |
3 | data class CompanyQuote(
4 | val changesPercentage: Double,
5 | val marketCap: Long,
6 | val pe: Double,
7 | val price: Double,
8 | val avgVolume: Int,
9 | val eps: Double
10 | )
11 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/investiq/domain/model/CompanyItem.kt:
--------------------------------------------------------------------------------
1 | package com.example.investiq.domain.model
2 |
3 | import androidx.room.PrimaryKey
4 |
5 | data class CompanyItem(
6 | val symbol:String,
7 | val name:String,
8 | val price:String,
9 | val exchangeShortName:String,
10 | val id :Int?=null
11 | )
12 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/investiq/data/local/StockDatabase.kt:
--------------------------------------------------------------------------------
1 | package com.example.investiq.data.local
2 |
3 | import androidx.room.Database
4 | import androidx.room.RoomDatabase
5 |
6 | @Database(entities = [CompanyItemEntity::class,FavCompanyEntity::class], version = 3)
7 | abstract class StockDatabase:RoomDatabase() {
8 | abstract val dao:StockDao
9 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/investiq/presentation/company_favorites/CompanyFavoriteState.kt:
--------------------------------------------------------------------------------
1 | package com.example.investiq.presentation.company_favorites
2 |
3 | import com.example.investiq.domain.model.CompanyFavItem
4 |
5 | data class CompanyFavoriteState(
6 | val isLoading:Boolean=false,
7 | val data:List = emptyList(),
8 | val error:String=""
9 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/example/investiq/data/local/CompanyItemEntity.kt:
--------------------------------------------------------------------------------
1 | package com.example.investiq.data.local
2 |
3 | import androidx.room.Entity
4 | import androidx.room.PrimaryKey
5 |
6 |
7 | @Entity
8 | data class CompanyItemEntity(
9 | val symbol:String,
10 | val name:String,
11 | val exchangeShortName:String,
12 | val price:String,
13 | @PrimaryKey val id:Int?=null
14 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/example/investiq/data/local/FavCompanyEntity.kt:
--------------------------------------------------------------------------------
1 | package com.example.investiq.data.local
2 |
3 | import androidx.room.Entity
4 | import androidx.room.PrimaryKey
5 |
6 | @Entity
7 | data class FavCompanyEntity(
8 | val symbol:String,
9 | val name:String,
10 | val exchangeShortName:String,
11 | val price:String,
12 | @PrimaryKey val id:Int?=null
13 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/example/investiq/util/Resource.kt:
--------------------------------------------------------------------------------
1 | package com.example.investiq.util
2 |
3 | sealed class Resource(val data: T? = null, val message: String? = null) {
4 | class Success(data: T?): Resource(data)
5 | class Error(message: String, data: T? = null): Resource(data, message)
6 | class Loading(val isLoading: Boolean = true): Resource(null)
7 | }
8 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/investiq/presentation/company_list/CompanyListingEvent.kt:
--------------------------------------------------------------------------------
1 | package com.example.investiq.presentation.company_list
2 |
3 | sealed class CompanyListingEvent(){
4 | // event simply means every single action an user can make in an ui this below are the changes an user can make
5 | object Refresh:CompanyListingEvent()
6 | data class OnSearchQueryChanged(val query:String):CompanyListingEvent()
7 | }
8 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/investiq/presentation/company_list/CompanyListingState.kt:
--------------------------------------------------------------------------------
1 | package com.example.investiq.presentation.company_list
2 |
3 | import com.example.investiq.domain.model.CompanyItem
4 |
5 | data class CompanyListingState(
6 | val companyList:List = emptyList(),
7 | val isRefreshing:Boolean=false,
8 | val isLoading:Boolean=false,
9 | val searchQuery:String = "",
10 | val error:String =""
11 | )
--------------------------------------------------------------------------------
/app/src/test/java/com/example/investiq/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.example.investiq
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/investiq/presentation/company_favorites/CompanyFavoriteEvent.kt:
--------------------------------------------------------------------------------
1 | package com.example.investiq.presentation.company_favorites
2 |
3 | import com.example.investiq.domain.model.CompanyFavItem
4 |
5 | sealed class CompanyFavoriteEvent {
6 | class insertData(val companyFavItem: CompanyFavItem):CompanyFavoriteEvent()
7 | class deleteData(val companyFavItem: CompanyFavItem):CompanyFavoriteEvent()
8 | data object getAllData : CompanyFavoriteEvent()
9 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/investiq/domain/respository/FavCompanyRepository.kt:
--------------------------------------------------------------------------------
1 | package com.example.investiq.domain.respository
2 |
3 | import com.example.investiq.domain.model.CompanyFavItem
4 | import kotlinx.coroutines.flow.Flow
5 |
6 | interface FavCompanyRepository {
7 |
8 | suspend fun insertData(companyFavItem: CompanyFavItem)
9 |
10 | suspend fun deleteData(companyFavItem: CompanyFavItem)
11 |
12 | fun getAllFavCompany():Flow>
13 |
14 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/investiq/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package com.example.investiq.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Purple80 = Color(0xFFD0BCFF)
6 | val PurpleGrey80 = Color(0xFFCCC2DC)
7 | val Pink80 = Color(0xFFEFB8C8)
8 |
9 | val Purple40 = Color(0xFF6650a4)
10 | val PurpleGrey40 = Color(0xFF625b71)
11 | val Pink40 = Color(0xFF7D5260)
12 | val Orange = Color(0xFFFD5F11)
13 | val Gold = Color(0xFFFFD700)
14 | val CustomGreen = Color(0xFF00966C)
15 | val CustomRed = Color(0xFFF44336)
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/investiq/presentation/company_info/CompanyInfoState.kt:
--------------------------------------------------------------------------------
1 | package com.example.investiq.presentation.company_info
2 |
3 | import com.example.investiq.domain.model.CompanyDetail
4 | import com.example.investiq.domain.model.CompanyQuote
5 | import com.example.investiq.domain.model.IntraDayInfo
6 |
7 | data class CompanyInfoState (
8 | val intraDayInfo: List = emptyList(),
9 | val companyDetail:CompanyDetail?=null,
10 | val stockPrice:CompanyQuote?=null,
11 | val isLoading:Boolean=false,
12 | val error:String?=null
13 | )
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google {
4 | content {
5 | includeGroupByRegex("com\\.android.*")
6 | includeGroupByRegex("com\\.google.*")
7 | includeGroupByRegex("androidx.*")
8 | }
9 | }
10 | mavenCentral()
11 | gradlePluginPortal()
12 | }
13 | }
14 | dependencyResolutionManagement {
15 | repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS)
16 | repositories {
17 | google()
18 | mavenCentral()
19 | }
20 | }
21 |
22 | rootProject.name = "InvestIQ"
23 | include(":app")
24 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/example/investiq/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.example.investiq
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.example.investiq", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/investiq/data/remote/dto/CompanyQuoteDto.kt:
--------------------------------------------------------------------------------
1 | package com.example.investiq.data.remote.dto
2 |
3 | data class CompanyQuoteDto(
4 | val avgVolume: Int?,
5 | val change: Double?,
6 | val changesPercentage: Double?,
7 | val dayHigh: Double?,
8 | val dayLow: Double?,
9 | val earningsAnnouncement: String?,
10 | val eps: Double?,
11 | val exchange: String?,
12 | val marketCap: Long?,
13 | val name: String?,
14 | val open: Double?,
15 | val pe: Double?,
16 | val previousClose: Double?,
17 | val price: Double?,
18 | val priceAvg200: Double?,
19 | val priceAvg50: Double?,
20 | val sharesOutstanding: Long?,
21 | val symbol: String?,
22 | val timestamp: Int?,
23 | val volume: Int?,
24 | val yearHigh: Double?,
25 | val yearLow: Double?
26 | )
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ## HEY THERE THIS ARE SOME CONTRIBUTION GUIDELINES.
2 |
3 | You can contribute if you are looking to start your journey in open source this can be great starting point. Start by making small changes.
4 |
5 | Once You become confident enough try to make some major changes in the app.
6 |
7 | If your changes wont break the app, will merge your pull request.
8 |
9 | U can also raise issue if u encounter any bugs or have a suggestion for any new feature
10 |
11 | In the end have fun with the project!!!
12 |
13 | ## Contributing:
14 | - Open an issue regarding proposed change.
15 | - If your proposed change is approved
16 | - Fork this repo and implement the changes.
17 | - Open PR. Add description(optional) in PR. You're done!
18 |
19 | ## How to Build :
20 | - Make sure to get ur api key from https://site.financialmodelingprep.com before forking the project
21 | - then add ur key in local.propertid (e.g = fmpApiKey = "ur api key")
22 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/investiq/domain/respository/StockRepository.kt:
--------------------------------------------------------------------------------
1 | package com.example.investiq.domain.respository
2 |
3 | import com.example.investiq.domain.model.CompanyDetail
4 | import com.example.investiq.domain.model.CompanyItem
5 | import com.example.investiq.domain.model.CompanyQuote
6 | import com.example.investiq.domain.model.IntraDayInfo
7 | import com.example.investiq.util.Resource
8 | import kotlinx.coroutines.flow.Flow
9 |
10 | interface StockRepository {
11 |
12 | suspend fun getCompanyListing(
13 | fetchFromRemote:Boolean, // should fetch data from remote if true or db if false
14 | query:String
15 | ): Flow>>
16 | // here we cant use List as domain and data layer should be independent of
17 | // each other and should depend upon model of their own layer
18 |
19 | suspend fun getCompanyInfo(symbol:String):Resource
20 |
21 | suspend fun getIntraDayInfo(symbol:String):Resource>
22 |
23 | suspend fun getCompanyPrice(symbol: String):Resource
24 | }
--------------------------------------------------------------------------------
/.idea/appInsightsSettings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/investiq/data/remote/dto/CompanyDetailDto.kt:
--------------------------------------------------------------------------------
1 | package com.example.investiq.data.remote.dto
2 |
3 | data class CompanyDetailDto(
4 | val address: String?,
5 | val beta: Double?,
6 | val ceo: String?,
7 | val changes: Double?,
8 | val cik: String?,
9 | val city: String?,
10 | val companyName: String?,
11 | val country: String?,
12 | val currency: String?,
13 | val cusip: String?,
14 | val dcf: Double?,
15 | val dcfDiff: Double?,
16 | val defaultImage: Boolean?,
17 | val description: String?,
18 | val exchange: String?,
19 | val exchangeShortName: String?,
20 | val fullTimeEmployees: String?,
21 | val image: String?,
22 | val industry: String?,
23 | val ipoDate: String?,
24 | val isActivelyTrading: Boolean?,
25 | val isAdr: Boolean?,
26 | val isEtf: Boolean?,
27 | val isFund: Boolean?,
28 | val isin: String?,
29 | val lastDiv: Double?,
30 | val mktCap: Long?,
31 | val phone: String?,
32 | val price: Double?,
33 | val range: String?,
34 | val sector: String?,
35 | val state: String?,
36 | val symbol: String?,
37 | val volAvg: Int?,
38 | val website: String?,
39 | val zip: String?
40 | )
--------------------------------------------------------------------------------
/.idea/deploymentTargetSelector.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/investiq/di/RepositoryModule.kt:
--------------------------------------------------------------------------------
1 | package com.example.investiq.di
2 |
3 | import com.example.investiq.data.repository.FavCompanyRepositoryImpl
4 | import com.example.investiq.data.repository.StockRepositoryImpl
5 | import com.example.investiq.domain.respository.FavCompanyRepository
6 | import com.example.investiq.domain.respository.StockRepository
7 | import dagger.Binds
8 | import dagger.Module
9 | import dagger.hilt.InstallIn
10 | import dagger.hilt.components.SingletonComponent
11 | import javax.inject.Singleton
12 |
13 |
14 | @Module
15 | @InstallIn(SingletonComponent::class)
16 | abstract class RepositoryModule {
17 |
18 | // when using @Inject constructor with Interface implementation u need to use @Bind to tell hilt how to
19 | // instantiate the interface
20 |
21 | // because an interface can have multiple classes implementing it, so hilt should know which class do u want the interface
22 | // to instantiate with
23 |
24 |
25 | @Singleton
26 | @Binds
27 | abstract fun bindsStockRepoImpl(stockRepositoryImpl: StockRepositoryImpl):StockRepository
28 |
29 | @Singleton
30 | @Binds
31 | abstract fun bindsFavCompanyRepoImpl(favCompanyRepositoryImpl: FavCompanyRepositoryImpl):FavCompanyRepository
32 |
33 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
18 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/investiq/data/local/StockDao.kt:
--------------------------------------------------------------------------------
1 | package com.example.investiq.data.local
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.room.Dao
5 | import androidx.room.Delete
6 | import androidx.room.Insert
7 | import androidx.room.OnConflictStrategy
8 | import androidx.room.Query
9 | import com.example.investiq.domain.model.CompanyItem
10 | import kotlinx.coroutines.flow.Flow
11 |
12 | @Dao
13 | interface StockDao {
14 |
15 | @Insert(onConflict = OnConflictStrategy.REPLACE)
16 | suspend fun insertCompanyListing(companyListings:List)
17 | @Query(" DELETE FROM companyitementity")
18 | suspend fun deleteAllCompanyListings()
19 | @Query(""" SELECT * FROM companyitementity
20 | WHERE LOWER(name) LIKE '%' || LOWER(:query) || '%'
21 | OR
22 | UPPER(:query) == symbol """)
23 | suspend fun searchForCompany(query:String):List
24 |
25 |
26 | @Insert(onConflict = OnConflictStrategy.REPLACE)
27 | suspend fun insertFavCompany(favCompanyEntity: FavCompanyEntity)
28 | @Delete
29 | suspend fun deleteFavCompany(favCompanyEntity: FavCompanyEntity)
30 | @Query(" SELECT * FROM FAVCOMPANYENTITY")
31 | fun getAllFavCompanyList():Flow>
32 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/investiq/data/repository/FavCompanyRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package com.example.investiq.data.repository
2 |
3 | import android.util.Log
4 | import com.example.investiq.data.local.StockDao
5 | import com.example.investiq.data.mappers.toFavCompanyEntity
6 | import com.example.investiq.data.mappers.toFavCompanyItem
7 | import com.example.investiq.domain.model.CompanyFavItem
8 | import com.example.investiq.domain.respository.FavCompanyRepository
9 | import kotlinx.coroutines.flow.Flow
10 | import kotlinx.coroutines.flow.map
11 | import javax.inject.Inject
12 |
13 | class FavCompanyRepositoryImpl @Inject constructor(private val dao:StockDao):FavCompanyRepository {
14 |
15 | override suspend fun insertData(companyFavItem: CompanyFavItem) {
16 | dao.insertFavCompany(companyFavItem.toFavCompanyEntity())
17 | }
18 |
19 | override suspend fun deleteData(companyFavItem: CompanyFavItem) {
20 | Log.d("FavCompRepoImpl","Delete fun is being called-$companyFavItem")
21 | dao.deleteFavCompany(companyFavItem.toFavCompanyEntity())
22 | }
23 |
24 | override fun getAllFavCompany(): Flow>{
25 | return dao.getAllFavCompanyList().map { companyEntityList ->
26 | companyEntityList.map { item ->
27 | item.toFavCompanyItem()
28 | }
29 | }
30 | }
31 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/investiq/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.example.investiq.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.Font
6 | import androidx.compose.ui.text.font.FontFamily
7 | import androidx.compose.ui.text.font.FontWeight
8 | import androidx.compose.ui.unit.sp
9 | import com.example.investiq.R
10 |
11 | // Set of Material typography styles to start with
12 | val Typography = Typography(
13 | bodyLarge = TextStyle(
14 | fontFamily = FontFamily.Default,
15 | fontWeight = FontWeight.Normal,
16 | fontSize = 16.sp,
17 | lineHeight = 24.sp,
18 | letterSpacing = 0.5.sp
19 | )
20 | /* Other default text styles to override
21 | titleLarge = TextStyle(
22 | fontFamily = FontFamily.Default,
23 | fontWeight = FontWeight.Normal,
24 | fontSize = 22.sp,
25 | lineHeight = 28.sp,
26 | letterSpacing = 0.sp
27 | ),
28 | labelSmall = TextStyle(
29 | fontFamily = FontFamily.Default,
30 | fontWeight = FontWeight.Medium,
31 | fontSize = 11.sp,
32 | lineHeight = 16.sp,
33 | letterSpacing = 0.5.sp
34 | )
35 | */
36 | )
37 |
38 |
39 | val poppins= FontFamily(
40 | Font(R.font.poppins_light, FontWeight.Light),
41 | Font(R.font.poppins_bold, FontWeight.Bold),
42 | Font(R.font.poppins_senibold, FontWeight.SemiBold),
43 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/example/investiq/di/AppModule.kt:
--------------------------------------------------------------------------------
1 | package com.example.investiq.di
2 |
3 | import android.app.Application
4 | import androidx.room.Room
5 | import com.example.investiq.constant.Constant
6 | import com.example.investiq.data.local.StockDao
7 | import com.example.investiq.data.local.StockDatabase
8 | import com.example.investiq.data.remote.StockApi
9 | import dagger.Module
10 | import dagger.Provides
11 | import dagger.hilt.InstallIn
12 | import dagger.hilt.components.SingletonComponent
13 | import retrofit2.Retrofit
14 | import retrofit2.converter.moshi.MoshiConverterFactory
15 | import retrofit2.create
16 | import javax.inject.Singleton
17 |
18 | @Module
19 | @InstallIn(SingletonComponent::class)
20 | object AppModule {
21 |
22 | @Provides
23 | @Singleton
24 | fun provideStockApi():StockApi{
25 | return Retrofit.Builder()
26 | .baseUrl(Constant.FMP_BASE_URL)
27 | .addConverterFactory(MoshiConverterFactory.create())
28 | .build()
29 | .create()
30 | }
31 |
32 | @Singleton
33 | @Provides
34 | fun providesStockDatabase(application: Application):StockDatabase{
35 | return Room.databaseBuilder(
36 | application
37 | ,StockDatabase::class.java
38 | ,"stock_database")
39 | .fallbackToDestructiveMigration()
40 | .build()
41 | }
42 |
43 |
44 | @Singleton
45 | @Provides
46 | fun providesDao(stockDatabase:StockDatabase):StockDao{
47 | return stockDatabase.dao
48 | }
49 |
50 |
51 | }
--------------------------------------------------------------------------------
/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. For more details, visit
12 | # https://developer.android.com/r/tools/gradle-multi-project-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 |
25 |
26 | # speeds up gradle build
27 | org.gradle.configuration-cache=true
28 | org.gradle.caching=true
29 | org.gradle.daemon=true
30 | org.gradle.parallel=true
31 | org.gradle.vfs.watch=true
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/investiq/data/remote/StockApi.kt:
--------------------------------------------------------------------------------
1 | package com.example.investiq.data.remote
2 |
3 |
4 | import com.example.investiq.BuildConfig
5 | import com.example.investiq.data.remote.dto.CompanyDetailDto
6 | import com.example.investiq.data.remote.dto.CompanyItemDto
7 | import com.example.investiq.data.remote.dto.CompanyQuoteDto
8 | import com.example.investiq.data.remote.dto.IntraDayDto
9 | import okhttp3.ResponseBody
10 | import retrofit2.http.GET
11 | import retrofit2.http.Path
12 | import retrofit2.http.Query
13 |
14 | interface StockApi {
15 |
16 |
17 | // Alpha Vantage Api
18 |
19 |
20 | @GET("query?function=TIME_SERIES_INTRADAY&interval=60min&datatype=csv")
21 | suspend fun getIntradayInfo(
22 | @Query("symbol") symbol: String, @Query("apikey") apiKey: String = BuildConfig.FMP_API_KEY
23 | ): ResponseBody
24 |
25 | // Financial Modeling Prep api
26 | @GET("api/v3/stock/list")
27 | suspend fun getCompanyList( @Query("apikey") apiKey: String=BuildConfig.FMP_API_KEY): List
28 |
29 | @GET("api/v3/profile/{symbol}")
30 | suspend fun getCompanyInfo(@Path("symbol") symbol:String ,@Query("apikey") apiKey:String=BuildConfig.FMP_API_KEY):List
31 |
32 | @GET("api/v3/quote/{symbol}")
33 | suspend fun getStockPrice(@Path("symbol") symbol:String,@Query("apikey") apiKey:String=BuildConfig.FMP_API_KEY):List
34 |
35 | @GET("api/v3/historical-chart/4hour/{symbol}")
36 | suspend fun getIntraDayInfo(@Path("symbol") symbol:String,
37 | @Query("from") from:String,
38 | @Query("to") to:String,
39 | @Query("apikey") apiKey: String = BuildConfig.FMP_API_KEY):List
40 |
41 | // https://financialmodelingprep.com/api/v3/historical-chart/1hour/AAPL?from=2024-05-07&to=2024-05-07&apikey=8v2NDAUDEkXY5gROnW9I6ycf5HGdT40l
42 |
43 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # InvestIq - Android App 📈
3 |
4 | 
5 |
6 | ### InvestIQ is built to demonstrate the use of Modern Android development tools.The app follows latest android principles and components
7 |
8 | **It's an app where you can view prices and detailed information about US stocks.**
9 |
10 | This project will be best for you if ur someone who are looking to learn more about android development or
11 |
12 | Who are looking for simpler projects to contribute to as they begin their open-source journey.
13 |
14 | ***Install the apk from here 👇***
15 |
16 | [](https://github.com/shalenMathew/InvestIq-AndroidApp/releases)
17 |
18 | ## App demo 📽️
19 | https://github.com/user-attachments/assets/1597b920-169c-45a6-8fa6-197f9eb76c26
20 |
21 | ## Built With 🛠
22 | - Kotlin
23 | - Jetpack Compose
24 | - Flow
25 | - Coroutines
26 | - Clean Architecture(MVVM)
27 | - Hilt
28 | - Retrofit
29 | - Room
30 |
31 | ## Features ✨
32 | - Get the prices of stocks listed in NASDAQ
33 | - Get detailed info about particular stocks
34 | - Get weekly price graphs
35 | - Search functionalty
36 | - Can save ur favorites stocks in watchlist named 'Favorites'
37 | - Offline support
38 |
39 | ## Contribution 🤝
40 | - If your new to open source and want to play around with contribution you can do it here or u can raise issue if you want to make any improvements or fix any bugs
41 | - Tip for beginners : Try to fix small bugs and dont get overwhelmed by the source code , take your time... Start by making small fixes or making small changes 👍
42 | - Read [contribution guidelines](CONTRIBUTING.md) before contributing
43 |
44 | ## Contact 📧
45 | Connect me using shalenmj@gmail.com
46 |
47 | Twitter - https://twitter.com/shalenMathew
48 |
49 | Linkedln - https://www.linkedin.com/in/shalen-mathew-3b566921b/
50 |
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | agp = "8.10.0"
3 | kotlin = "1.9.25"
4 | coreKtx = "1.15.0"
5 | junit = "4.13.2"
6 | junitVersion = "1.2.1"
7 | espressoCore = "3.6.1"
8 | lifecycleRuntimeKtx = "2.8.7"
9 | activityCompose = "1.10.0"
10 | composeBom = "2025.01.01"
11 | material3Android = "1.3.1"
12 |
13 | [libraries]
14 | androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
15 | junit = { group = "junit", name = "junit", version.ref = "junit" }
16 | androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
17 | androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
18 | androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
19 | androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
20 | androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
21 | androidx-ui = { group = "androidx.compose.ui", name = "ui" }
22 | androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
23 | androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
24 | androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
25 | androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
26 | androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
27 | androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
28 | androidx-material3-android = { group = "androidx.compose.material3", name = "material3-android", version.ref = "material3Android" }
29 |
30 | [plugins]
31 | android-application = { id = "com.android.application", version.ref = "agp" }
32 | jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
33 |
34 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/investiq/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.example.investiq.ui.theme
2 |
3 | import android.app.Activity
4 | import android.os.Build
5 | import androidx.compose.foundation.isSystemInDarkTheme
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.material3.darkColorScheme
8 | import androidx.compose.material3.dynamicDarkColorScheme
9 | import androidx.compose.material3.dynamicLightColorScheme
10 | import androidx.compose.material3.lightColorScheme
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.ui.graphics.Color
13 | import androidx.compose.ui.platform.LocalContext
14 |
15 | private val DarkColorScheme = darkColorScheme(
16 | primary = Purple80,
17 | secondary = PurpleGrey80,
18 | tertiary = Pink80
19 |
20 | )
21 |
22 | private val LightColorScheme = lightColorScheme(
23 | primary = Purple40,
24 | secondary = PurpleGrey40,
25 | tertiary = Pink40
26 |
27 | /* Other default colors to override
28 | background = Color(0xFFFFFBFE),
29 | surface = Color(0xFFFFFBFE),
30 | onPrimary = Color.White,
31 | onSecondary = Color.White,
32 | onTertiary = Color.White,
33 | onBackground = Color(0xFF1C1B1F),
34 | onSurface = Color(0xFF1C1B1F),
35 | */
36 | )
37 |
38 | @Composable
39 | fun InvestIQTheme(
40 | darkTheme: Boolean = isSystemInDarkTheme(),
41 | // Dynamic color is available on Android 12+
42 | dynamicColor: Boolean = true,
43 | content: @Composable () -> Unit
44 | ) {
45 | val colorScheme = when {
46 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
47 | val context = LocalContext.current
48 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
49 | }
50 |
51 | darkTheme -> DarkColorScheme
52 | else -> LightColorScheme
53 | }
54 |
55 | MaterialTheme(
56 | colorScheme = colorScheme,
57 | typography = Typography,
58 | content = content
59 | )
60 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/investiq/presentation/company_favorites/CompanyFavoriteViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.example.investiq.presentation.company_favorites
2 |
3 | import androidx.compose.runtime.getValue
4 | import androidx.compose.runtime.mutableStateOf
5 | import androidx.compose.runtime.setValue
6 | import androidx.lifecycle.ViewModel
7 | import androidx.lifecycle.viewModelScope
8 | import com.example.investiq.domain.respository.FavCompanyRepository
9 | import dagger.hilt.android.lifecycle.HiltViewModel
10 | import kotlinx.coroutines.Job
11 | import kotlinx.coroutines.flow.launchIn
12 | import kotlinx.coroutines.flow.onEach
13 | import kotlinx.coroutines.launch
14 | import javax.inject.Inject
15 |
16 |
17 | @HiltViewModel
18 | class CompanyFavoriteViewModel @Inject constructor(private val favCompanyRepository: FavCompanyRepository):ViewModel() {
19 |
20 | var state by mutableStateOf(CompanyFavoriteState())
21 | private var getFavCompJob:Job? = null
22 |
23 | init {
24 | getAllFavCompany()
25 | }
26 |
27 | fun onEvent(event: CompanyFavoriteEvent){
28 |
29 | when(event){
30 |
31 | is CompanyFavoriteEvent.insertData->{
32 |
33 | viewModelScope.launch {
34 | favCompanyRepository.insertData(event.companyFavItem)
35 | }
36 |
37 | }
38 | is CompanyFavoriteEvent.deleteData->{
39 |
40 | viewModelScope.launch {
41 | favCompanyRepository.deleteData(event.companyFavItem)
42 | }
43 |
44 | }
45 | is CompanyFavoriteEvent.getAllData->{
46 | getAllFavCompany()
47 | }
48 |
49 | }
50 |
51 | }
52 |
53 |
54 | private fun getAllFavCompany(){
55 |
56 | // -> launchIn is used to launch the collection of the Flow inside a specific coroutine scope.
57 | // Instead of manually calling collect, launchIn automatically starts collecting the Flow in the scope you specify.
58 | getFavCompJob?.cancel()
59 |
60 | state = state.copy(isLoading = true)
61 |
62 | getFavCompJob = viewModelScope.launch {
63 |
64 | favCompanyRepository.getAllFavCompany().onEach { dataList->
65 | state = state.copy(isLoading = false, data = dataList)
66 | }.launchIn(this)
67 | }
68 |
69 | }
70 |
71 |
72 | }
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/investiq/presentation/company_list/CompanyListingViewmodel.kt:
--------------------------------------------------------------------------------
1 | package com.example.investiq.presentation.company_list
2 |
3 | import android.util.Log
4 | import androidx.compose.runtime.getValue
5 | import androidx.compose.runtime.mutableStateOf
6 | import androidx.compose.runtime.setValue
7 | import androidx.lifecycle.ViewModel
8 | import androidx.lifecycle.viewModelScope
9 | import com.example.investiq.domain.respository.StockRepository
10 | import com.example.investiq.util.Resource
11 | import dagger.hilt.android.lifecycle.HiltViewModel
12 | import kotlinx.coroutines.Dispatchers
13 | import kotlinx.coroutines.Job
14 | import kotlinx.coroutines.delay
15 | import kotlinx.coroutines.launch
16 | import kotlinx.coroutines.withContext
17 | import javax.inject.Inject
18 |
19 |
20 | @HiltViewModel
21 | class CompanyListingViewmodel @Inject constructor(private val stockRepository: StockRepository):ViewModel() {
22 |
23 | var state by mutableStateOf(CompanyListingState())
24 |
25 | private var searchJob: Job?=null
26 |
27 | init {
28 | getCompanyListing()
29 | }
30 |
31 | fun getCompanyListing(
32 | query:String = state.searchQuery.lowercase(),
33 | fetchFromRemote:Boolean = false
34 | ){
35 | viewModelScope.launch(Dispatchers.IO) {
36 |
37 | Log.d("TAG",Thread.currentThread().name)
38 | stockRepository.getCompanyListing(fetchFromRemote, query).collect{
39 |
40 | withContext(Dispatchers.Main){
41 |
42 | Log.d("TAG",Thread.currentThread().name)
43 |
44 | when(it){
45 |
46 | is Resource.Success->{
47 | state = if (it.data.isNullOrEmpty()){
48 | state.copy(isLoading = false,error="Nothing here to display... TRy to Refresh")
49 | }else{
50 | state.copy(isLoading = false,companyList = it.data, error = "")
51 | }
52 | }
53 | is Resource.Loading->{
54 | state=state.copy(isLoading = it.isLoading)
55 | }
56 |
57 | is Resource.Error-> {
58 | state=state.copy(error=it.message!!, isLoading = false)
59 | }
60 |
61 | }
62 | }
63 | }
64 | }
65 | }
66 |
67 | fun onEvent(companyListingEvent: CompanyListingEvent){
68 |
69 | when(companyListingEvent){
70 |
71 | is CompanyListingEvent.OnSearchQueryChanged->{
72 |
73 | state = state.copy(searchQuery = companyListingEvent.query)
74 |
75 | searchJob?.cancel()
76 |
77 | searchJob = viewModelScope.launch {
78 | delay(500L)
79 | getCompanyListing()
80 | }
81 |
82 | }
83 |
84 | is CompanyListingEvent.Refresh->{
85 | getCompanyListing(fetchFromRemote = true)
86 | }
87 | }
88 | }
89 |
90 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/investiq/presentation/company_info/CompanyInfoViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.example.investiq.presentation.company_info
2 |
3 | import androidx.compose.runtime.getValue
4 | import androidx.compose.runtime.mutableStateOf
5 | import androidx.compose.runtime.setValue
6 | import androidx.lifecycle.SavedStateHandle
7 | import androidx.lifecycle.ViewModel
8 | import androidx.lifecycle.viewModelScope
9 | import com.example.investiq.domain.respository.StockRepository
10 | import com.example.investiq.util.Resource
11 | import dagger.hilt.android.lifecycle.HiltViewModel
12 | import kotlinx.coroutines.async
13 | import kotlinx.coroutines.launch
14 | import javax.inject.Inject
15 |
16 |
17 | @HiltViewModel
18 | class CompanyInfoViewModel @Inject constructor(
19 | private val stockRepository: StockRepository,
20 | private val savedStateHandle: SavedStateHandle
21 | ):ViewModel() {
22 |
23 | var state:CompanyInfoState by mutableStateOf(CompanyInfoState())
24 |
25 | init {
26 | getCompanyInfo()
27 | }
28 |
29 | private fun getCompanyInfo(){
30 |
31 | viewModelScope.launch {
32 | val symbol = savedStateHandle.get("symbol") ?: return@launch
33 |
34 | state = state.copy(isLoading = true)
35 |
36 | // val companyInfo = stockRepository.getCompanyInfo(symbol)
37 | // val intraDayInfo = stockRepository.getIntraDayInfo(symbol)
38 | // as we know can call this thread blocking fun in coroutine
39 | // but as we the know coroutine runs line by line which means we wont get result for intraDayInfo until
40 | // we get the the result for companyInfo , as we can see both are unrelated task so we can call this simultaneously
41 |
42 | val companyInfoResult = async {stockRepository.getCompanyInfo(symbol)}
43 | val intraDayInfoResult = async {stockRepository.getIntraDayInfo(symbol)}
44 | val stockPrice = async { stockRepository.getCompanyPrice(symbol) }
45 |
46 | when(val result = companyInfoResult.await()){
47 | is Resource.Error -> state=state.copy(isLoading = false, error = result.message, companyDetail = null)
48 | is Resource.Loading -> state= state.copy(isLoading = true, companyDetail = null, error = null)
49 | is Resource.Success -> {
50 | state = state.copy(companyDetail = result.data, isLoading = false, error = null)
51 | }
52 | }
53 |
54 | when(val result = intraDayInfoResult.await()){
55 | is Resource.Error -> state=state.copy(isLoading = false, error = result.message, companyDetail = null)
56 | is Resource.Loading -> state= state.copy(isLoading = true, companyDetail = null, error = null)
57 | is Resource.Success -> {
58 | state = state.copy(intraDayInfo = result.data ?: emptyList(), isLoading = false, error = null )
59 | }
60 | }
61 |
62 | when(val result = stockPrice.await() ){
63 | is Resource.Error -> state=state.copy(isLoading = false, error = result.message,companyDetail = null)
64 | is Resource.Loading -> state= state.copy(isLoading = true, companyDetail = null, error = null)
65 | is Resource.Success -> {
66 | state = state.copy(stockPrice= result.data , isLoading = false, error = null )
67 | }
68 | }
69 |
70 | }
71 | }
72 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/investiq/presentation/company_favorites/CompanyFavouriteScreen.kt:
--------------------------------------------------------------------------------
1 | package com.example.investiq.presentation.company_favorites
2 |
3 | import android.util.Log
4 | import androidx.compose.foundation.background
5 | import androidx.compose.foundation.border
6 | import androidx.compose.foundation.layout.Box
7 | import androidx.compose.foundation.layout.Column
8 | import androidx.compose.foundation.layout.fillMaxSize
9 | import androidx.compose.foundation.layout.fillMaxWidth
10 | import androidx.compose.foundation.layout.padding
11 | import androidx.compose.foundation.layout.wrapContentHeight
12 | import androidx.compose.foundation.layout.wrapContentWidth
13 | import androidx.compose.foundation.lazy.LazyColumn
14 | import androidx.compose.foundation.lazy.itemsIndexed
15 | import androidx.compose.foundation.shape.RoundedCornerShape
16 | import androidx.compose.material.Text
17 | import androidx.compose.material.pullrefresh.pullRefresh
18 | import androidx.compose.runtime.Composable
19 | import androidx.compose.ui.Alignment
20 | import androidx.compose.ui.Modifier
21 | import androidx.compose.ui.graphics.Color
22 | import androidx.compose.ui.platform.LocalContext
23 | import androidx.compose.ui.text.font.FontWeight
24 | import androidx.compose.ui.text.style.TextAlign
25 | import androidx.compose.ui.tooling.preview.Preview
26 | import androidx.compose.ui.unit.dp
27 | import androidx.compose.ui.unit.sp
28 | import androidx.hilt.navigation.compose.hiltViewModel
29 | import androidx.navigation.NavController
30 | import com.example.investiq.domain.model.CompanyFavItem
31 | import com.example.investiq.presentation.company_list.CompanyListingViewmodel
32 | import com.example.investiq.presentation.screens.Screen
33 |
34 |
35 | @Composable
36 | fun FavouriteCompanyScreen(
37 | navigator: NavController,
38 | companyFavoriteViewModel: CompanyFavoriteViewModel= hiltViewModel()
39 | ){
40 |
41 | val state = companyFavoriteViewModel.state
42 |
43 |
44 | Box(modifier= Modifier
45 | .fillMaxSize()
46 | ) {
47 |
48 | Column(modifier=Modifier.fillMaxSize()) {
49 |
50 |
51 | Box(modifier = Modifier.fillMaxWidth(),
52 | ){
53 | Text("Favorites",
54 | color = Color.Black,
55 | fontWeight = FontWeight.SemiBold,
56 | modifier = Modifier.padding(start = 12.dp, top = 12.dp, bottom = 12.dp),
57 | fontSize = 25.sp)
58 | }
59 | if(state.data.isNotEmpty()){
60 |
61 | LazyColumn(modifier = Modifier.fillMaxSize()) {
62 |
63 | itemsIndexed(state.data){ index: Int, item: CompanyFavItem ->
64 |
65 | Log.d("CompanyFavScreen", "Fetched data: $item")
66 |
67 | CompanyFavoriteItem(company=item, onClick = { navigator.navigate(Screen.FavoritesScreen.route+"/${item.symbol}") },
68 | viewModel = companyFavoriteViewModel)
69 | }
70 | }
71 |
72 | }
73 | else{
74 |
75 | Box(modifier= Modifier
76 | .padding(12.dp)
77 | .fillMaxSize()
78 | .padding(10.dp)
79 | , contentAlignment = Alignment.Center
80 | ){
81 | Text(text = "Nothing in Favorites Yet!!!",
82 | color = Color.Black,
83 | fontSize = 15.sp
84 | )
85 | }
86 |
87 | }
88 | }
89 |
90 | }
91 |
92 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/investiq/data/mappers/CompanyMappers.kt:
--------------------------------------------------------------------------------
1 | package com.example.investiq.data.mappers
2 |
3 |
4 | import com.example.investiq.data.local.CompanyItemEntity
5 | import com.example.investiq.data.local.FavCompanyEntity
6 | import com.example.investiq.data.remote.dto.CompanyDetailDto
7 | import com.example.investiq.data.remote.dto.CompanyItemDto
8 | import com.example.investiq.data.remote.dto.CompanyQuoteDto
9 | import com.example.investiq.data.remote.dto.IntraDayDto
10 | import com.example.investiq.domain.model.CompanyDetail
11 | import com.example.investiq.domain.model.CompanyItem
12 | import com.example.investiq.domain.model.CompanyQuote
13 | import com.example.investiq.domain.model.CompanyFavItem
14 | import com.example.investiq.domain.model.IntraDayInfo
15 | import java.time.LocalDateTime
16 | import java.time.format.DateTimeFormatter
17 | import java.util.Locale
18 |
19 |
20 | /// now making mappers that will map or convert the given dto class in data layer to model class in domain layer
21 | // (abstraction)
22 |
23 | fun CompanyFavItem.toFavCompanyEntity():FavCompanyEntity{
24 |
25 | return FavCompanyEntity(
26 | symbol = symbol,
27 | name = name,
28 | exchangeShortName = exchangeShortName,
29 | price = price,
30 | id=id
31 | )
32 | }
33 |
34 | fun CompanyItem.toCompanyFavItem():CompanyFavItem{
35 |
36 | return CompanyFavItem(
37 | symbol = symbol,
38 | name = name,
39 | exchangeShortName = exchangeShortName,
40 | price = price,
41 | id=id
42 | )
43 |
44 | }
45 |
46 |
47 | fun FavCompanyEntity.toFavCompanyItem():CompanyFavItem{
48 |
49 | return CompanyFavItem(symbol = symbol, name = name,price=price, exchangeShortName =exchangeShortName , id = id)
50 |
51 | }
52 |
53 | fun CompanyItemEntity.toCompanyItem():CompanyItem{
54 | return CompanyItem(
55 | symbol=symbol,
56 | name=name,
57 | exchangeShortName=exchangeShortName,
58 | price = price,
59 | id=id
60 | )
61 | }
62 |
63 | fun CompanyItem.toCompanyListingEntity():CompanyItemEntity{
64 |
65 | return CompanyItemEntity(
66 | symbol=symbol ?: "nullFromMappers",
67 | name=name ?: "null",
68 | exchangeShortName=exchangeShortName ?: "nullFromMappers",
69 | price = price ?: "nullFromMappers"
70 | )
71 | }
72 |
73 |
74 | fun IntraDayDto.toIntraDayInfo():IntraDayInfo{
75 |
76 | val pattern = "yyyy-MM-dd HH:mm:ss"
77 | val formatter = DateTimeFormatter.ofPattern(pattern, Locale.getDefault())
78 | val localTimeFormatter = LocalDateTime.parse(date,formatter)
79 |
80 | return IntraDayInfo(
81 | close = close ?: 0.0,
82 | date =localTimeFormatter
83 | )
84 | }
85 |
86 | fun CompanyDetailDto.toCompanyDetail():CompanyDetail{
87 |
88 | return CompanyDetail(
89 | companyName = companyName ?: "N/a from mappers" ,
90 | symbol = symbol ?: "N/A",
91 | image = image ?: "" ,
92 | dcf = dcf ?: 0.00,
93 | )
94 | }
95 |
96 | fun CompanyItemDto.toCompanyItem():CompanyItem{
97 |
98 | return CompanyItem(
99 | symbol = symbol ?: "nullFromMappers" ,
100 | name = name ?: "null",
101 | price = price ?:"null",
102 | exchangeShortName= exchangeShortName ?:"null"
103 | )
104 | }
105 |
106 |
107 | fun CompanyQuoteDto.toCompanyQuote():CompanyQuote{
108 | return CompanyQuote(
109 | changesPercentage = changesPercentage ?: 0.0,
110 | marketCap = marketCap ?: 0,
111 | pe = pe ?: 0.0 ,
112 | price = price ?: 0.0,
113 | avgVolume= avgVolume ?: 0,
114 | eps=eps?:0.0
115 | )
116 | }
117 |
118 |
119 |
120 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/investiq/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.example.investiq
2 |
3 | import android.os.Build
4 | import android.os.Bundle
5 | import android.util.Log
6 | import androidx.activity.ComponentActivity
7 | import androidx.activity.compose.setContent
8 | import androidx.compose.foundation.background
9 | import androidx.compose.foundation.layout.Column
10 | import androidx.compose.foundation.layout.fillMaxSize
11 | import androidx.compose.foundation.layout.statusBarsPadding
12 | import androidx.compose.material.Scaffold
13 | import androidx.compose.material3.MaterialTheme
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.runtime.SideEffect
16 | import androidx.compose.runtime.getValue
17 | import androidx.compose.runtime.remember
18 | import androidx.compose.ui.Modifier
19 | import androidx.compose.ui.graphics.Color
20 | import androidx.compose.ui.unit.dp
21 | import androidx.navigation.compose.NavHost
22 | import androidx.navigation.compose.composable
23 | import androidx.navigation.compose.currentBackStackEntryAsState
24 | import androidx.navigation.compose.rememberNavController
25 | import com.example.investiq.presentation.company_info.CompanyInfoScreen
26 | import com.example.investiq.presentation.company_list.CompanyListScreen
27 | import com.example.investiq.presentation.bottomNav.BottomBarTab
28 | import com.example.investiq.presentation.bottomNav.CustomBottomNavigation
29 | import com.example.investiq.presentation.company_favorites.FavouriteCompanyScreen
30 | import com.example.investiq.presentation.screens.Screen
31 | import com.example.investiq.ui.theme.InvestIQTheme
32 | import com.google.accompanist.systemuicontroller.rememberSystemUiController
33 | import dagger.hilt.android.AndroidEntryPoint
34 | import dev.chrisbanes.haze.HazeState
35 | import dev.chrisbanes.haze.haze
36 |
37 |
38 | @AndroidEntryPoint
39 | class MainActivity : ComponentActivity() {
40 | override fun onCreate(savedInstanceState: Bundle?) {
41 | super.onCreate(savedInstanceState)
42 | setContent {
43 | InvestIQTheme {
44 |
45 | // SetBarColor(color = Color.Transparent)
46 |
47 | val hazeState = remember { HazeState() }
48 |
49 | val navController = rememberNavController()
50 |
51 |
52 | Scaffold(modifier = Modifier.fillMaxSize()
53 | .background(color = Color.White)
54 | .statusBarsPadding(),
55 | bottomBar = {
56 |
57 | val navBackStackEntry by navController.currentBackStackEntryAsState()
58 |
59 | // Get the current route
60 | val currentDestination = navBackStackEntry?.destination?.route
61 |
62 | Log.d("DES",currentDestination ?:"Null")
63 | if (currentDestination.equals("Home") || currentDestination.equals("Favourites")) {
64 | CustomBottomNavigation(hazeState,navController)
65 | }
66 | }) { padding ->
67 |
68 | Column( Modifier
69 | .haze(
70 | hazeState,
71 | backgroundColor = MaterialTheme.colorScheme.background,
72 | tint = if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.S_V2) Color.Black.copy(alpha = .1f) else Color.Black.copy(alpha = 1f) ,
73 | blurRadius = 30.dp,
74 | )
75 | .fillMaxSize()) {
76 |
77 |
78 | NavHost(navController = navController, startDestination = BottomBarTab.Home.name ) {
79 |
80 | composable(Screen.FavoritesScreen.route+"/{symbol}"){ CompanyInfoScreen() }
81 | composable(BottomBarTab.Home.name){ CompanyListScreen(navController) }
82 | composable(BottomBarTab.Favourites.name){ FavouriteCompanyScreen(navController) }
83 |
84 | }
85 | }
86 |
87 | }
88 |
89 | }
90 | }
91 | }
92 |
93 | @Composable
94 | fun SetBarColor(color: Color){
95 | val systemUiController = rememberSystemUiController()
96 | SideEffect {
97 | systemUiController.setSystemBarsColor(
98 | color = color
99 | )
100 | }
101 | }
102 | }
103 |
104 |
105 |
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import java.util.Properties
2 |
3 | plugins {
4 | alias(libs.plugins.android.application)
5 | alias(libs.plugins.jetbrains.kotlin.android)
6 |
7 | id ("dagger.hilt.android.plugin")
8 | id ("kotlin-parcelize")
9 | id("com.google.devtools.ksp")
10 | id("kotlin-kapt")
11 | }
12 |
13 | android {
14 |
15 | namespace = "com.example.investiq"
16 | compileSdk = 35
17 |
18 | defaultConfig {
19 | applicationId = "com.example.investiq"
20 | minSdk = 26
21 | targetSdk = 35
22 | versionCode = 1
23 | versionName = "1.0"
24 |
25 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
26 | vectorDrawables {
27 | useSupportLibrary = true
28 | }
29 |
30 | val properties = Properties().apply {
31 | load(project.rootProject.file("local.properties").inputStream())
32 | }
33 | val fmpApiKey = properties.getProperty("fmpApiKey")
34 |
35 | buildConfigField("String","FMP_API_KEY",fmpApiKey)
36 |
37 | }
38 |
39 | buildTypes {
40 | release {
41 | isMinifyEnabled = false
42 | proguardFiles(
43 | getDefaultProguardFile("proguard-android-optimize.txt"),
44 | "proguard-rules.pro"
45 | )
46 | }
47 |
48 | debug{
49 | applicationIdSuffix=".debug"
50 | isDebuggable = true
51 | }
52 |
53 | }
54 | compileOptions {
55 | sourceCompatibility = JavaVersion.VERSION_17
56 | targetCompatibility = JavaVersion.VERSION_17
57 | }
58 | kotlinOptions {
59 | jvmTarget = "17"
60 | }
61 | buildFeatures {
62 | compose = true
63 | buildConfig = true
64 | }
65 | composeOptions {
66 | kotlinCompilerExtensionVersion = "1.5.15"
67 | }
68 | packaging {
69 | resources {
70 | excludes += "/META-INF/{AL2.0,LGPL2.1}"
71 | }
72 | }
73 | }
74 |
75 | dependencies {
76 |
77 | implementation(libs.androidx.core.ktx)
78 | implementation(libs.androidx.lifecycle.runtime.ktx)
79 | implementation(libs.androidx.activity.compose)
80 | implementation(platform(libs.androidx.compose.bom))
81 | implementation(libs.androidx.ui)
82 | implementation(libs.androidx.ui.graphics)
83 | implementation(libs.androidx.ui.tooling.preview)
84 | implementation ("androidx.compose.material:material:1.7.7") //
85 | implementation(libs.androidx.material3.android)
86 | testImplementation(libs.junit)
87 | androidTestImplementation(libs.androidx.junit)
88 | androidTestImplementation(libs.androidx.espresso.core)
89 | androidTestImplementation(platform(libs.androidx.compose.bom))
90 | androidTestImplementation(libs.androidx.ui.test.junit4)
91 | debugImplementation(libs.androidx.ui.tooling)
92 | debugImplementation(libs.androidx.ui.test.manifest)
93 |
94 | // OpenCSV
95 | implementation ("com.opencsv:opencsv:5.5.2")
96 |
97 | implementation ("com.google.accompanist:accompanist-flowlayout:0.17.0")
98 |
99 | implementation ("androidx.paging:paging-compose:3.3.0")
100 | implementation ("com.google.accompanist:accompanist-swiperefresh:0.24.2-alpha")
101 |
102 |
103 | // Coil
104 | implementation ("io.coil-kt:coil-compose:2.4.0")
105 |
106 | //Dagger - Hilt
107 | implementation ("com.google.dagger:hilt-android:2.49")
108 | ksp ("com.google.dagger:hilt-android-compiler:2.49")
109 | implementation ("androidx.hilt:hilt-navigation-compose:1.2.0")
110 |
111 | // Retrofit
112 | implementation ("com.squareup.retrofit2:retrofit:2.9.0")
113 | implementation ("com.squareup.retrofit2:converter-moshi:2.9.0")
114 | implementation ("com.squareup.okhttp3:okhttp:5.0.0-alpha.3")
115 | implementation ("com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.3")
116 |
117 | // Room
118 | implementation ("androidx.room:room-runtime:2.6.1")
119 | ksp ("androidx.room:room-compiler:2.6.1")
120 |
121 | // Kotlin Extensions and Coroutines support for Room
122 | implementation ("androidx.room:room-ktx:2.6.1")
123 |
124 | // system UI Controller
125 | implementation("com.google.accompanist:accompanist-systemuicontroller:0.27.0")
126 |
127 |
128 | // Swipe item to delete
129 | implementation ("me.saket.swipe:swipe:1.3.0")
130 |
131 | //lottie
132 | implementation("com.airbnb.android:lottie-compose:6.3.0")
133 |
134 | // haze effect
135 | implementation("dev.chrisbanes.haze:haze-jetpack-compose:0.4.1")
136 |
137 | }
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
10 |
12 |
14 |
16 |
18 |
20 |
22 |
24 |
26 |
28 |
30 |
32 |
34 |
36 |
38 |
40 |
42 |
44 |
46 |
48 |
50 |
52 |
54 |
56 |
58 |
60 |
62 |
64 |
66 |
68 |
70 |
72 |
74 |
75 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/investiq/presentation/company_favorites/CompanyFavoriteItem.kt:
--------------------------------------------------------------------------------
1 | package com.example.investiq.presentation.company_favorites
2 |
3 | import android.widget.Toast
4 | import androidx.compose.foundation.background
5 | import androidx.compose.foundation.clickable
6 | import androidx.compose.foundation.layout.Box
7 | import androidx.compose.foundation.layout.Column
8 | import androidx.compose.foundation.layout.Row
9 | import androidx.compose.foundation.layout.fillMaxWidth
10 | import androidx.compose.foundation.layout.padding
11 | import androidx.compose.foundation.layout.wrapContentHeight
12 | import androidx.compose.foundation.shape.RoundedCornerShape
13 | import androidx.compose.material.Text
14 | import androidx.compose.material.icons.Icons
15 | import androidx.compose.material.icons.filled.Star
16 | import androidx.compose.material.icons.twotone.Star
17 | import androidx.compose.runtime.Composable
18 | import androidx.compose.ui.Alignment
19 | import androidx.compose.ui.Modifier
20 | import androidx.compose.ui.draw.clip
21 | import androidx.compose.ui.graphics.Color
22 | import androidx.compose.ui.graphics.vector.rememberVectorPainter
23 | import androidx.compose.ui.platform.LocalContext
24 | import androidx.compose.ui.text.font.FontWeight
25 | import androidx.compose.ui.text.style.TextOverflow
26 | import androidx.compose.ui.unit.dp
27 | import androidx.compose.ui.unit.sp
28 | import com.example.investiq.domain.model.CompanyFavItem
29 | import com.example.investiq.domain.model.CompanyItem
30 | import com.example.investiq.ui.theme.CustomGreen
31 | import com.example.investiq.ui.theme.CustomRed
32 | import com.example.investiq.ui.theme.Gold
33 | import com.example.investiq.ui.theme.poppins
34 | import me.saket.swipe.SwipeAction
35 | import me.saket.swipe.SwipeableActionsBox
36 |
37 | @Composable
38 | fun CompanyFavoriteItem(
39 | company: CompanyFavItem,
40 | onClick:()->Unit,
41 | viewModel: CompanyFavoriteViewModel
42 | ){
43 | val context = LocalContext.current
44 |
45 | // val save = SwipeAction(
46 | // icon = rememberVectorPainter(Icons.Filled.Star),
47 | // background = CustomGreen,
48 | // onSwipe = {
49 | //
50 | // }
51 | // )
52 |
53 | val unsave = SwipeAction(
54 | icon = rememberVectorPainter(Icons.TwoTone.Star),
55 | background = CustomRed,
56 | onSwipe = {
57 | viewModel.onEvent(CompanyFavoriteEvent.deleteData(company))
58 | Toast.makeText(context, "Removed from favorites", Toast.LENGTH_SHORT).show()
59 | }
60 | )
61 |
62 |
63 | SwipeableActionsBox(
64 | startActions = listOf(unsave),
65 | // endActions = listOf(save),
66 | swipeThreshold = 65.dp
67 | ) {
68 |
69 | Box(
70 | modifier= Modifier
71 | .padding(start = 5.dp, end = 5.dp, bottom = 5.dp)
72 | .clip(RoundedCornerShape(18.dp))
73 | .background(color = Color.Black)
74 | .fillMaxWidth()
75 | .wrapContentHeight()
76 | .clickable {
77 | onClick()
78 | },
79 | contentAlignment = Alignment.Center
80 | ){
81 |
82 | Row(
83 | modifier= Modifier
84 | .fillMaxWidth()
85 | .wrapContentHeight(),
86 | verticalAlignment = Alignment.CenterVertically
87 | ){
88 |
89 | Column(modifier= Modifier.weight(1f)) {
90 |
91 | Text(text = company.name,
92 | color = Color.White,
93 | fontSize = 18.sp,
94 | modifier = Modifier.padding(top=15.dp, start=15.dp, end = 10.dp, bottom = 2.dp),
95 | fontWeight = FontWeight.SemiBold,
96 | fontFamily = poppins,
97 | maxLines = 1,
98 | overflow = TextOverflow.Ellipsis)
99 |
100 | Text(text = company.symbol,
101 | color = Color.White,
102 | fontSize = 15.sp,
103 | modifier = Modifier.padding(top=2.dp, start=15.dp, end = 10.dp, bottom = 15.dp),
104 | fontWeight = FontWeight.Medium,
105 | fontFamily = poppins,
106 | maxLines = 1,
107 | overflow = TextOverflow.Ellipsis)
108 |
109 |
110 | }
111 |
112 | Column {
113 | Text(text = "$ " + company.price,
114 | color = Gold,
115 | fontSize = 18.sp,
116 | fontFamily = poppins,
117 | fontWeight = FontWeight.SemiBold,
118 | modifier= Modifier.padding(end=15.dp, start = 12.dp, bottom = 2.dp))
119 |
120 | Text(text = company.exchangeShortName,
121 | color = Color.White,
122 | fontSize = 12.sp,
123 | fontFamily = poppins,
124 | fontWeight = FontWeight.Medium,
125 | modifier= Modifier.padding(end=15.dp, start = 12.dp, top = 2.dp))
126 |
127 | }
128 |
129 | }
130 | }
131 | }
132 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/investiq/presentation/company_info/StockChart.kt:
--------------------------------------------------------------------------------
1 | package com.example.investiq.presentation.company_info
2 |
3 | import android.graphics.Paint
4 | import android.os.Build
5 | import android.util.Log
6 | import androidx.annotation.RequiresApi
7 | import androidx.compose.foundation.Canvas
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.runtime.remember
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.graphics.Brush
12 | import androidx.compose.ui.graphics.Color
13 | import androidx.compose.ui.graphics.Path
14 | import androidx.compose.ui.graphics.StrokeCap
15 | import androidx.compose.ui.graphics.asAndroidPath
16 | import androidx.compose.ui.graphics.asComposePath
17 | import androidx.compose.ui.graphics.drawscope.Stroke
18 | import androidx.compose.ui.graphics.nativeCanvas
19 | import androidx.compose.ui.platform.LocalDensity
20 | import androidx.compose.ui.unit.dp
21 | import androidx.compose.ui.unit.sp
22 | import com.example.investiq.domain.model.IntraDayInfo
23 | import java.time.format.TextStyle
24 | import java.util.Locale
25 | import kotlin.math.round
26 | import kotlin.math.roundToInt
27 |
28 |
29 | @Composable
30 | fun StockChart(
31 | modifier: Modifier=Modifier,
32 | infos:List = emptyList(),
33 | graphColor:Color= Color.Green
34 | ) {
35 |
36 | val spacing = 100f
37 | val transparentGraphColor = remember {
38 | graphColor.copy(alpha = 0.5f)
39 | }
40 |
41 | val upperValue = remember(infos) {
42 | (infos.maxOfOrNull { it.close }?.plus(1))?.roundToInt() ?: 0
43 | }
44 |
45 | val lowerValue = remember(infos) {
46 | infos.minOfOrNull { it.close }?.toInt() ?: 0
47 | }
48 |
49 | // creating text design
50 | val density = LocalDensity.current
51 | val textPaint = remember(density) {
52 | Paint().apply {
53 | color = android.graphics.Color.WHITE
54 | textAlign = Paint.Align.CENTER
55 | textSize = density.run { 12.sp.toPx() }
56 | }
57 | }
58 |
59 | // setting up day text in x direction
60 | Canvas(modifier = modifier) {
61 |
62 | val filteredInfos = infos.filterIndexed { index, _ ->
63 | Log.d("infos",infos[index].date.dayOfWeek.toString())
64 | (index + 1) % 2 != 0
65 | }
66 |
67 | val chartWidth = (size.width-spacing)/infos.size
68 | // val spacerPerWeek = (size.width-spacing)/(infos.size-filteredInfos.size)
69 | val spacerPerWeek = (size.width-spacing)/(filteredInfos.size)
70 |
71 | (0 until filteredInfos.size).forEach(){
72 | val info=filteredInfos[it]
73 | val hour = info.date.dayOfWeek
74 |
75 | // Format the day of the week to its short form
76 | val shortDayOfWeek: String = hour.getDisplayName(TextStyle.SHORT, Locale.ENGLISH)
77 | Log.d("hour", shortDayOfWeek)
78 |
79 | drawContext.canvas.nativeCanvas.apply {
80 | drawText(
81 | shortDayOfWeek,
82 | spacing + (it*spacerPerWeek)+50,
83 | size.height-5,
84 | textPaint
85 | )
86 | }
87 | }
88 |
89 | val priceStep = (upperValue - lowerValue) / 5f
90 |
91 | (0..4).forEach { i ->
92 | drawContext.canvas.nativeCanvas.apply {
93 | drawText(
94 | round(lowerValue + priceStep * i).toString(),
95 | 30f,
96 | size.height - spacing - i * size.height / 5f,
97 | textPaint
98 | )
99 | }
100 | }
101 |
102 | var lastX = 0f
103 | val strokePath = Path().apply {
104 | val height = size.height
105 |
106 | for ( i in infos.indices){
107 |
108 | val info = infos[i]
109 | val nextInfo = infos.getOrNull(i+1) ?: infos.last()
110 |
111 | val leftRatio = (info.close - lowerValue)/ (upperValue-lowerValue)
112 | val rightRatio = (nextInfo.close - lowerValue)/ (upperValue-lowerValue)
113 |
114 | val x1 = spacing + i * chartWidth
115 | val y1 = height - spacing - (leftRatio * height).toFloat()
116 | val x2 = spacing + (i + 1) * chartWidth
117 | val y2 = height - spacing - (rightRatio * height).toFloat()
118 | if(i == 0) {
119 | moveTo(x1, y1)
120 | }
121 | lastX = (x1 + x2) / 2f
122 | quadraticBezierTo(
123 | x1, y1, lastX, (y1 + y2) / 2f
124 | )
125 | }
126 | }
127 |
128 | val fillPath = android.graphics.Path(strokePath.asAndroidPath())
129 | .asComposePath()
130 | .apply {
131 | lineTo(lastX, size.height - spacing)
132 | lineTo(spacing, size.height - spacing)
133 | close()
134 | }
135 |
136 | drawPath(
137 | path = fillPath,
138 | brush = Brush.verticalGradient(
139 | colors = listOf(
140 | transparentGraphColor,
141 | Color.Transparent
142 | ),
143 | endY = size.height - spacing
144 | )
145 | )
146 |
147 | drawPath(
148 | path = strokePath,
149 | color = graphColor,
150 | style = Stroke(
151 | width = 3.dp.toPx(),
152 | cap = StrokeCap.Round
153 | )
154 | )
155 |
156 | }
157 |
158 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/investiq/presentation/company_list/CompanyItem.kt:
--------------------------------------------------------------------------------
1 | package com.example.investiq.presentation.company_list
2 |
3 |
4 | import android.widget.Toast
5 | import androidx.compose.foundation.background
6 | import androidx.compose.foundation.clickable
7 | import androidx.compose.foundation.layout.Box
8 | import androidx.compose.foundation.layout.Column
9 | import androidx.compose.foundation.layout.Row
10 | import androidx.compose.foundation.layout.fillMaxWidth
11 | import androidx.compose.foundation.layout.padding
12 | import androidx.compose.foundation.layout.wrapContentHeight
13 | import androidx.compose.foundation.shape.RoundedCornerShape
14 | import androidx.compose.material.Text
15 | import androidx.compose.material.icons.Icons
16 | import androidx.compose.material.icons.filled.Star
17 | import androidx.compose.material.icons.twotone.Star
18 | import androidx.compose.runtime.Composable
19 | import androidx.compose.ui.Alignment
20 | import androidx.compose.ui.Modifier
21 | import androidx.compose.ui.draw.clip
22 | import androidx.compose.ui.graphics.Color
23 | import androidx.compose.ui.graphics.vector.rememberVectorPainter
24 | import androidx.compose.ui.platform.LocalContext
25 | import androidx.compose.ui.text.font.FontWeight
26 | import androidx.compose.ui.text.style.TextOverflow
27 | import androidx.compose.ui.unit.dp
28 | import androidx.compose.ui.unit.sp
29 | import com.example.investiq.data.mappers.toCompanyFavItem
30 | import com.example.investiq.domain.model.CompanyItem
31 | import com.example.investiq.presentation.company_favorites.CompanyFavoriteEvent
32 | import com.example.investiq.presentation.company_favorites.CompanyFavoriteViewModel
33 | import com.example.investiq.ui.theme.CustomGreen
34 | import com.example.investiq.ui.theme.CustomRed
35 | import com.example.investiq.ui.theme.Gold
36 | import com.example.investiq.ui.theme.poppins
37 | import me.saket.swipe.SwipeAction
38 | import me.saket.swipe.SwipeableActionsBox
39 |
40 |
41 | @Composable
42 | fun CompanyItem(
43 | modifier: Modifier,
44 | company:CompanyItem,
45 | onClick:()->Unit,
46 | viewmodel:CompanyFavoriteViewModel
47 | ){
48 |
49 | val context = LocalContext.current
50 |
51 | val save = SwipeAction(
52 | icon = rememberVectorPainter(Icons.Filled.Star),
53 | background = CustomGreen,
54 | onSwipe = {
55 | viewmodel.onEvent(CompanyFavoriteEvent.insertData(company.toCompanyFavItem()))
56 | Toast.makeText(context, "Added to ur favorites", Toast.LENGTH_SHORT).show()
57 | }
58 | )
59 |
60 | val unsave = SwipeAction(
61 | icon = rememberVectorPainter(Icons.TwoTone.Star),
62 | background = CustomRed,
63 | onSwipe = {
64 | viewmodel.onEvent(CompanyFavoriteEvent.deleteData(company.toCompanyFavItem()))
65 | Toast.makeText(context, "Removed from favorites", Toast.LENGTH_SHORT).show()
66 | }
67 | )
68 |
69 |
70 | SwipeableActionsBox(
71 | startActions = listOf(unsave),
72 | endActions = listOf(save),
73 | swipeThreshold = 65.dp
74 | ) {
75 |
76 | Box(
77 | modifier= modifier
78 | .padding(start = 5.dp, end = 5.dp, bottom = 5.dp)
79 | .clip(RoundedCornerShape(18.dp))
80 | .background(color = Color.Black)
81 | .fillMaxWidth()
82 | .wrapContentHeight()
83 | .clickable {
84 | onClick()
85 | },
86 | contentAlignment = Alignment.Center
87 | ){
88 |
89 | Row(
90 | modifier= Modifier
91 | .fillMaxWidth()
92 | .wrapContentHeight(),
93 | verticalAlignment = Alignment.CenterVertically
94 | ){
95 |
96 | Column(modifier=Modifier.weight(1f)) {
97 |
98 | Text(text = company.name,
99 | color = Color.White,
100 | fontSize = 18.sp,
101 | modifier = Modifier.padding(top=15.dp, start=15.dp, end = 10.dp, bottom = 2.dp),
102 | fontWeight = FontWeight.SemiBold,
103 | fontFamily = poppins,
104 | maxLines = 1,
105 | overflow = TextOverflow.Ellipsis)
106 |
107 | Text(text = company.symbol,
108 | color = Color.White,
109 | fontSize = 15.sp,
110 | modifier = Modifier.padding(top=2.dp, start=15.dp, end = 10.dp, bottom = 15.dp),
111 | fontWeight = FontWeight.Medium,
112 | fontFamily = poppins,
113 | maxLines = 1,
114 | overflow = TextOverflow.Ellipsis)
115 |
116 |
117 | }
118 |
119 | Column {
120 | Text(text = "$ " + company.price,
121 | color = Gold,
122 | fontSize = 18.sp,
123 | fontFamily = poppins,
124 | fontWeight = FontWeight.SemiBold,
125 | modifier=Modifier.padding(end=15.dp, start = 12.dp, bottom = 2.dp))
126 |
127 | Text(text = company.exchangeShortName,
128 | color = Color.White,
129 | fontSize = 12.sp,
130 | fontFamily = poppins,
131 | fontWeight = FontWeight.Medium,
132 | modifier=Modifier.padding(end=15.dp, start = 12.dp, top = 2.dp))
133 |
134 | }
135 |
136 | }
137 | }
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | shalenmj@gmail.com.
64 | All complaints will be reviewed and investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series
86 | of actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or
93 | permanent ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within
113 | the community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.0, available at
119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120 |
121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
122 | enforcement ladder](https://github.com/mozilla/diversity).
123 |
124 | [homepage]: https://www.contributor-covenant.org
125 |
126 | For answers to common questions about this code of conduct, see the FAQ at
127 | https://www.contributor-covenant.org/faq. Translations are available at
128 | https://www.contributor-covenant.org/translations.
129 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/investiq/data/repository/StockRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package com.example.investiq.data.repository
2 |
3 | import android.os.Build
4 | import android.util.Log
5 | import androidx.annotation.RequiresApi
6 | import com.example.investiq.data.local.StockDatabase
7 | import com.example.investiq.data.mappers.toCompanyDetail
8 |
9 | import com.example.investiq.data.mappers.toCompanyItem
10 | import com.example.investiq.data.mappers.toCompanyListingEntity
11 | import com.example.investiq.data.mappers.toCompanyQuote
12 | import com.example.investiq.data.mappers.toIntraDayInfo
13 | import com.example.investiq.data.remote.StockApi
14 | import com.example.investiq.data.remote.dto.CompanyItemDto
15 | import com.example.investiq.domain.model.CompanyDetail
16 | import com.example.investiq.domain.model.CompanyItem
17 | import com.example.investiq.domain.model.CompanyQuote
18 | import com.example.investiq.domain.model.IntraDayInfo
19 | import com.example.investiq.domain.respository.StockRepository
20 | import com.example.investiq.util.Resource
21 | import kotlinx.coroutines.flow.Flow
22 | import kotlinx.coroutines.flow.flow
23 | import retrofit2.HttpException
24 | import java.io.IOException
25 | import java.net.UnknownHostException
26 | import java.time.LocalDate
27 | import javax.inject.Inject
28 |
29 | class StockRepositoryImpl @Inject constructor(
30 | private val stockApi: StockApi,
31 | private val stockDb:StockDatabase,
32 | ):StockRepository {
33 |
34 | private val dao = stockDb.dao
35 |
36 | override suspend fun getCompanyListing(
37 | fetchFromRemote: Boolean,
38 | query: String
39 | ):Flow>> {
40 |
41 | return flow {
42 |
43 | emit(Resource.Loading(isLoading = true))
44 |
45 | // taking data from cache
46 | val localListings = dao.searchForCompany(query)
47 |
48 | // checking if db is empty ...
49 | val isDbEmpty = localListings.isEmpty() && query.isBlank()
50 | val shouldJustLoadFromCache = !isDbEmpty && !fetchFromRemote // if db is not empty and and not fetchFromRemote we will
51 | // just get data from cache one issue that can arise from this is we can only make limited call from this api
52 | // once the limit is over api will return empty list instead of an error message indicating that limit it is over
53 |
54 | // now if the db is empty it wont get from cache anymore as it will skip the below condition
55 |
56 | // Log.d("TAG_from_StockRepoImpl", "fetch from remote - $fetchFromRemote ")
57 | // Log.d("TAG_from_StockRepoImpl","shouldJustLoadFromCache - $shouldJustLoadFromCache")
58 |
59 | if (shouldJustLoadFromCache){
60 | emit(Resource.Loading(false))
61 | Log.d("TAG", "data from db ")
62 |
63 | emit(Resource.Success(localListings.map {
64 | it.toCompanyItem()
65 | }))
66 |
67 | return@flow // this will return only from flow not from entire function
68 | // return from here coz , no need to fetch from api if the cache is not empty
69 | }
70 |
71 | Log.d("FLOW","before api call")
72 |
73 | val remoteListings = try {
74 |
75 | stockApi.getCompanyList()
76 |
77 | } catch(e: IOException) {
78 | e.printStackTrace()
79 | emit(Resource.Error(e.message ?: "IO error"))
80 | null
81 | } catch (e: HttpException) {
82 | e.printStackTrace()
83 | emit(Resource.Error(e.message ?: "unknown error occurred"))
84 | null
85 | } catch (e:UnknownHostException){
86 | e.printStackTrace()
87 | emit(Resource.Error("No network connection...Try to refresh..."))
88 | null
89 | } catch (e:Exception){
90 | e.printStackTrace()
91 | emit(Resource.Error(e.message ?: "unknown error occurred"))
92 | null
93 | }
94 | Log.d("FLOW","after api call")
95 |
96 | // now caching the data we gotten from our api
97 |
98 |
99 |
100 | // deleting existing data if any
101 | remoteListings?.let { listOfcompanyListing->
102 |
103 | val sampleList = mutableListOf()
104 |
105 | Log.d("FLOW","before delete")
106 |
107 | dao.deleteAllCompanyListings()
108 | // here down our insert fun in from data layer so should only take model from data layer
109 |
110 | Log.d("FLOW","after delete delete")
111 |
112 | listOfcompanyListing.filter {
113 | it.exchangeShortName== "NASDAQ"
114 | }.forEach { item->
115 | sampleList.add(item)
116 | }
117 |
118 |
119 | Log.d("FLOW","LIST SIZE - ${sampleList.size}")
120 | Log.d("FLOW","Before insert")
121 | // inserting new data
122 | dao.insertCompanyListing(
123 | sampleList.map {
124 | it.toCompanyItem().toCompanyListingEntity()
125 | }
126 | )
127 |
128 | Log.d("FLOW","after insert")
129 |
130 | Log.d("FLOW","before emit")
131 |
132 | // emitting the data
133 | emit(Resource.Success(
134 | dao.searchForCompany("").map {
135 | it.toCompanyItem()
136 | }
137 | ))
138 |
139 | Log.d("FLOW","after emit")
140 |
141 | emit(Resource.Loading(false))
142 |
143 | }?:run { Log.d("stock_repo_impl","remote listing is null") }
144 | }
145 |
146 | }
147 |
148 | @RequiresApi(Build.VERSION_CODES.O)
149 | override suspend fun getIntraDayInfo(symbol: String): Resource> {
150 |
151 | return try{
152 | val from = LocalDate.now().minusDays(7).toString()
153 | val to = LocalDate.now().minusDays(3).toString()
154 | Log.d("from",from)
155 | Log.d("to",to)
156 | val intraDayInfoResult = stockApi.getIntraDayInfo(symbol=symbol, from =from , to = to)
157 |
158 | Resource.Success(intraDayInfoResult.map { it.toIntraDayInfo() })
159 |
160 | }catch (e:Exception){
161 | return Resource.Error(message = e.message.toString())
162 | }
163 |
164 | }
165 |
166 | override suspend fun getCompanyInfo(symbol: String): Resource {
167 | try {
168 | val companyDetailResult = stockApi.getCompanyInfo(symbol)
169 | return Resource.Success(data = companyDetailResult[0].toCompanyDetail())
170 |
171 | }catch (e:Exception){
172 | return Resource.Error(message = e.message.toString())
173 | }
174 | }
175 |
176 | override suspend fun getCompanyPrice(symbol: String): Resource {
177 | return try {
178 | val stockPriceResult = stockApi.getStockPrice(symbol)
179 | Resource.Success(data = stockPriceResult[0].toCompanyQuote())
180 | }catch (e:Exception){
181 | Resource.Error(message = e.message.toString())
182 | }
183 | }
184 |
185 |
186 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/investiq/presentation/bottomNav/BottomBarTab.kt:
--------------------------------------------------------------------------------
1 | package com.example.investiq.presentation.bottomNav
2 |
3 | import androidx.compose.animation.animateColorAsState
4 | import androidx.compose.animation.core.Spring
5 | import androidx.compose.animation.core.animateFloatAsState
6 | import androidx.compose.animation.core.spring
7 | import androidx.compose.foundation.Canvas
8 | import androidx.compose.foundation.background
9 | import androidx.compose.foundation.border
10 | import androidx.compose.foundation.gestures.detectTapGestures
11 | import androidx.compose.foundation.layout.Arrangement
12 | import androidx.compose.foundation.layout.Box
13 | import androidx.compose.foundation.layout.Column
14 | import androidx.compose.foundation.layout.Row
15 | import androidx.compose.foundation.layout.fillMaxHeight
16 | import androidx.compose.foundation.layout.fillMaxSize
17 | import androidx.compose.foundation.layout.fillMaxWidth
18 | import androidx.compose.foundation.layout.height
19 | import androidx.compose.foundation.layout.padding
20 | import androidx.compose.foundation.shape.CircleShape
21 | import androidx.compose.material.icons.Icons
22 | import androidx.compose.material.icons.rounded.Home
23 | import androidx.compose.material.icons.rounded.Star
24 | import androidx.compose.material3.Icon
25 | import androidx.compose.material3.LocalContentColor
26 | import androidx.compose.material3.LocalTextStyle
27 | import androidx.compose.material3.Text
28 | import androidx.compose.runtime.Composable
29 | import androidx.compose.runtime.CompositionLocalProvider
30 | import androidx.compose.runtime.getValue
31 | import androidx.compose.runtime.mutableIntStateOf
32 | import androidx.compose.runtime.remember
33 | import androidx.compose.runtime.setValue
34 | import androidx.compose.ui.Alignment
35 | import androidx.compose.ui.Modifier
36 | import androidx.compose.ui.draw.BlurredEdgeTreatment
37 | import androidx.compose.ui.draw.alpha
38 | import androidx.compose.ui.draw.blur
39 | import androidx.compose.ui.draw.clip
40 | import androidx.compose.ui.draw.scale
41 | import androidx.compose.ui.geometry.CornerRadius
42 | import androidx.compose.ui.geometry.Offset
43 | import androidx.compose.ui.geometry.RoundRect
44 | import androidx.compose.ui.geometry.toRect
45 | import androidx.compose.ui.graphics.Brush
46 | import androidx.compose.ui.graphics.Color
47 | import androidx.compose.ui.graphics.Path
48 | import androidx.compose.ui.graphics.PathEffect
49 | import androidx.compose.ui.graphics.PathMeasure
50 | import androidx.compose.ui.graphics.drawscope.Stroke
51 | import androidx.compose.ui.graphics.vector.ImageVector
52 | import androidx.compose.ui.hapticfeedback.HapticFeedbackType
53 | import androidx.compose.ui.input.pointer.pointerInput
54 | import androidx.compose.ui.platform.LocalHapticFeedback
55 | import androidx.compose.ui.text.font.FontWeight
56 | import androidx.compose.ui.unit.Dp
57 | import androidx.compose.ui.unit.dp
58 | import androidx.compose.ui.unit.sp
59 | import androidx.navigation.NavController
60 | import com.example.investiq.ui.theme.Orange
61 | import dev.chrisbanes.haze.HazeState
62 | import dev.chrisbanes.haze.hazeChild
63 |
64 | sealed class BottomBarTab ( val name:String,val icon:ImageVector,val color:Color){
65 | data object Home: BottomBarTab("Home", Icons.Rounded.Home,Color(0xFFF86529))
66 | data object Favourites: BottomBarTab("Favourites",Icons.Rounded.Star,Color(0xFFADFF64))
67 | }
68 |
69 | @Composable
70 | fun CustomBottomNavigation(hazeState: HazeState,navigator:NavController){
71 |
72 | val tabs= listOf(
73 | BottomBarTab.Home,
74 | BottomBarTab.Favourites
75 | )
76 |
77 | var selectedTabIndex by remember { mutableIntStateOf(0) }
78 |
79 | Box(modifier = Modifier
80 | .padding( horizontal = 64.dp, vertical = 30.dp)
81 | .fillMaxWidth()
82 | .height(75.dp)
83 | .hazeChild(state = hazeState,shape = CircleShape)
84 | .border(width = Dp.Hairline,
85 | brush = Brush.verticalGradient(
86 | colors = listOf(Color.White.copy(alpha = 1f)
87 | ,Color.White.copy(alpha = .3f))
88 | ), shape = CircleShape))
89 | {
90 | BottomBarTabs(tabs,selectedTabIndex, onTabSelected = {
91 | selectedTabIndex = tabs.indexOf(it)
92 | navigator.navigate(it.name){
93 | popUpTo(navigator.graph.id){
94 | inclusive=true
95 | }
96 | launchSingleTop=true
97 | }
98 | })
99 |
100 | val animatedSelectedTabIndex by animateFloatAsState(
101 | targetValue = selectedTabIndex.toFloat(),
102 | label = "animatedSelectedTabIndex",
103 | animationSpec = spring(
104 | stiffness = Spring.StiffnessLow,
105 | dampingRatio = Spring.DampingRatioLowBouncy,
106 | )
107 | )
108 |
109 | val animatedColor by animateColorAsState(
110 | targetValue = tabs[selectedTabIndex].color,
111 | label = "animatedColor",
112 | animationSpec = spring(
113 | stiffness = Spring.StiffnessLow,
114 | )
115 | )
116 |
117 | Canvas(
118 | modifier = Modifier
119 | .fillMaxSize()
120 | .clip(CircleShape)
121 | .blur(50.dp, edgeTreatment = BlurredEdgeTreatment.Unbounded)
122 | ) {
123 | val tabWidth = size.width / tabs.size
124 | drawCircle(
125 | color = animatedColor.copy(alpha = .6f),
126 | radius = size.height / 2,
127 | center = Offset(
128 | (tabWidth * animatedSelectedTabIndex) + tabWidth / 2,
129 | size.height /2
130 | )
131 | )
132 | }
133 |
134 | Canvas(
135 | modifier = Modifier
136 | .fillMaxSize()
137 | .clip(CircleShape)
138 | ) {
139 | val path = Path().apply {
140 | addRoundRect(RoundRect(size.toRect(), CornerRadius(size.height)))
141 | }
142 | val length = PathMeasure().apply { setPath(path, false) }.length
143 |
144 | val tabWidth = size.width / tabs.size
145 | drawPath(
146 | path,
147 | brush = Brush.horizontalGradient(
148 | colors = listOf(
149 | animatedColor.copy(alpha = 0f),
150 | animatedColor.copy(alpha = 1f),
151 | animatedColor.copy(alpha = 1f),
152 | animatedColor.copy(alpha = 0f),
153 | ),
154 | startX = tabWidth * animatedSelectedTabIndex,
155 | endX = tabWidth * (animatedSelectedTabIndex + 1),
156 | ),
157 | style = Stroke(
158 | width = 6f,
159 | pathEffect = PathEffect.dashPathEffect(
160 | intervals = floatArrayOf(length / 2, length)
161 | )
162 | )
163 | )
164 | }
165 |
166 | }
167 | }
168 |
169 | @Composable
170 | fun BottomBarTabs(
171 | tabs: List,
172 | selectedTab: Int,
173 | onTabSelected: (BottomBarTab) -> Unit,
174 | ) {
175 |
176 | val hapticFeedback = LocalHapticFeedback.current
177 |
178 | CompositionLocalProvider(
179 | LocalTextStyle provides LocalTextStyle.current.copy(
180 | fontSize = 12.sp,
181 | fontWeight = FontWeight.Medium,
182 | ),
183 | LocalContentColor provides Color.White
184 | ) {
185 | Row(
186 | modifier = Modifier.fillMaxSize(),
187 | ) {
188 | for (tab in tabs) {
189 | val alpha by animateFloatAsState(
190 | targetValue = if (selectedTab == tabs.indexOf(tab)) 1f else .35f,
191 | label = "alpha"
192 | )
193 | val scale by animateFloatAsState(
194 | targetValue = if (selectedTab == tabs.indexOf(tab)) 1f else .95f,
195 | visibilityThreshold = .000001f,
196 | animationSpec = spring(
197 | stiffness = Spring.StiffnessLow,
198 | dampingRatio = Spring.DampingRatioMediumBouncy,
199 | ),
200 | label = "scale"
201 | )
202 |
203 | Column(
204 | modifier = Modifier
205 | .scale(scale)
206 | .alpha(alpha)
207 | .fillMaxHeight()
208 | .weight(1f)
209 | .pointerInput(Unit) {
210 | detectTapGestures {
211 | hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
212 | onTabSelected(tab)
213 | }
214 | },
215 | horizontalAlignment = Alignment.CenterHorizontally,
216 | verticalArrangement = Arrangement.Center,
217 | ) {
218 | Icon(imageVector = tab.icon, contentDescription = "tab ${tab.name}")
219 | Text(text = tab.name)
220 | }
221 | }
222 | }
223 | }
224 |
225 | }
226 |
227 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/investiq/presentation/company_info/CompanyInfoScreen.kt:
--------------------------------------------------------------------------------
1 | package com.example.investiq.presentation.company_info
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.layout.Box
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.Row
7 | import androidx.compose.foundation.layout.Spacer
8 | import androidx.compose.foundation.layout.fillMaxSize
9 | import androidx.compose.foundation.layout.fillMaxWidth
10 | import androidx.compose.foundation.layout.height
11 | import androidx.compose.foundation.layout.padding
12 | import androidx.compose.foundation.layout.wrapContentHeight
13 | import androidx.compose.foundation.layout.wrapContentSize
14 | import androidx.compose.foundation.rememberScrollState
15 | import androidx.compose.foundation.verticalScroll
16 | import androidx.compose.material.CircularProgressIndicator
17 | import androidx.compose.material.Text
18 | import androidx.compose.runtime.Composable
19 | import androidx.compose.ui.Alignment
20 | import androidx.compose.ui.Modifier
21 | import androidx.compose.ui.graphics.Color
22 | import androidx.compose.ui.text.font.FontWeight
23 | import androidx.compose.ui.text.style.TextOverflow
24 | import androidx.compose.ui.unit.dp
25 | import androidx.compose.ui.unit.sp
26 | import androidx.hilt.navigation.compose.hiltViewModel
27 | import com.example.investiq.ui.theme.Orange
28 | import com.example.investiq.ui.theme.poppins
29 |
30 |
31 | @Composable
32 | fun CompanyInfoScreen(
33 | viewModel: CompanyInfoViewModel= hiltViewModel()
34 | ){
35 |
36 | val state = viewModel.state
37 |
38 | if(state.error==null ){
39 |
40 | Column(modifier= Modifier
41 | .fillMaxSize()
42 | .background(color = Color.Black)
43 | .verticalScroll(rememberScrollState())) {
44 |
45 | if (state.stockPrice!=null){
46 | state.companyDetail?.let { companyDetail ->
47 |
48 | Box(modifier = Modifier
49 | .fillMaxWidth()
50 | .wrapContentHeight(),
51 | contentAlignment = Alignment.Center){
52 |
53 | Column(modifier = Modifier
54 | .wrapContentSize()
55 | .background(color = Color.Black)) {
56 |
57 | Text(text = companyDetail.symbol ,
58 | color = Color.White,
59 | fontSize = 20.sp,
60 | modifier = Modifier
61 | .padding(top = 15.dp)
62 | .align(Alignment.CenterHorizontally),
63 | fontWeight = FontWeight.Medium,
64 | fontFamily = poppins,
65 | )
66 |
67 | Text(text = companyDetail.companyName,
68 | color = Color.White,
69 | fontSize = 25.sp,
70 | modifier = Modifier
71 | .padding(bottom = 5.dp, start = 20.dp, end = 20.dp)
72 | .align(Alignment.CenterHorizontally),
73 | fontWeight = FontWeight.SemiBold,
74 | fontFamily = poppins,
75 | maxLines = 1,
76 | overflow = TextOverflow.Ellipsis
77 | )
78 |
79 | }
80 | }
81 |
82 | Box(modifier = Modifier
83 | .fillMaxWidth()
84 | .wrapContentHeight(),
85 | contentAlignment = Alignment.Center){
86 |
87 | Column(modifier = Modifier
88 | .wrapContentSize()
89 | ) {
90 |
91 | state.stockPrice.let { quote->
92 |
93 | Text(text = "$ "+quote.price,
94 | color = Color.White,
95 | fontSize = 30.sp,
96 | modifier = Modifier
97 | .padding(top = 15.dp)
98 | .align(Alignment.CenterHorizontally),
99 | fontWeight = FontWeight.Bold,
100 | fontFamily = poppins,
101 | )
102 |
103 |
104 | if(quote.changesPercentage.toString().first() == '-'){
105 |
106 | Text(text = quote.changesPercentage.toString()+"%",
107 | color = Color.Red,
108 | fontSize = 18.sp,
109 | modifier = Modifier
110 | .padding(top = 2.dp)
111 | .align(Alignment.CenterHorizontally),
112 | fontWeight = FontWeight.SemiBold,
113 | fontFamily = poppins,
114 | )
115 |
116 | }else{
117 | Text(text = "+"+quote.changesPercentage+"%",
118 | color = Color.Green,
119 | fontSize = 18.sp,
120 | modifier = Modifier
121 | .padding(top = 2.dp)
122 | .align(Alignment.CenterHorizontally),
123 | fontWeight = FontWeight.SemiBold,
124 | fontFamily = poppins,
125 | )
126 | }
127 |
128 | }
129 |
130 | }
131 | }
132 |
133 |
134 |
135 | Column(modifier = Modifier.fillMaxWidth()) {
136 |
137 | Spacer(modifier = Modifier.height(30.dp))
138 |
139 | Text(text ="Chart Summary",
140 | modifier = Modifier
141 | .align(Alignment.CenterHorizontally),
142 | fontSize = 15.sp,
143 | fontWeight = FontWeight.SemiBold,
144 | fontFamily = poppins,
145 | color = Color.White)
146 |
147 | Spacer(modifier = Modifier.height(30.dp))
148 |
149 | StockChart(infos = state.intraDayInfo.reversed(), modifier = Modifier
150 | .padding(start = 5.dp, top = 15.dp)
151 | .fillMaxWidth()
152 | .height(250.dp))
153 |
154 | Spacer(modifier = Modifier.height(10.dp))
155 |
156 | Text(text ="Fundamentals",
157 | modifier = Modifier
158 | .align(Alignment.Start)
159 | .padding(start = 12.dp, top = 20.dp),
160 | fontSize = 15.sp,
161 | fontWeight = FontWeight.SemiBold,
162 | fontFamily = poppins,
163 | color = Color.White)
164 |
165 | Spacer(modifier = Modifier.height(5.dp))
166 |
167 | state.stockPrice.let { quote->
168 | Row(modifier = Modifier.padding(top = 2.dp)){
169 | Text(text ="Market Capitalization : ",
170 | modifier = Modifier
171 | .padding(start = 12.dp),
172 | fontSize = 15.sp,
173 | fontWeight = FontWeight.Medium,
174 | fontFamily = poppins,
175 | color = Color.LightGray)
176 |
177 | Text(text ="$ " +quote.marketCap.toString(),
178 | modifier = Modifier,
179 | fontSize = 15.sp,
180 | fontWeight = FontWeight.Medium,
181 | fontFamily = poppins,
182 | color = Color.White)
183 |
184 | }
185 | Row(modifier = Modifier.padding(top = 2.dp)){
186 | Text(text ="Average Volume : ",
187 | modifier = Modifier
188 | .padding(start = 12.dp),
189 | fontSize = 15.sp,
190 | fontWeight = FontWeight.Medium,
191 | fontFamily = poppins,
192 | color = Color.LightGray)
193 |
194 | Text(text =quote.avgVolume.toString(),
195 | modifier = Modifier,
196 | fontSize = 15.sp,
197 | fontWeight = FontWeight.Medium,
198 | fontFamily = poppins,
199 | color = Color.White)
200 |
201 | }
202 | Row(modifier = Modifier.padding(top = 2.dp)){
203 | Text(text ="Price to Earning Ratio : ",
204 | modifier = Modifier
205 | .padding(start = 12.dp),
206 | fontSize = 15.sp,
207 | fontWeight = FontWeight.Medium,
208 | fontFamily = poppins,
209 | color = Color.LightGray)
210 |
211 | Text(text =quote.pe.toString(),
212 | modifier = Modifier,
213 | fontSize = 15.sp,
214 | fontWeight = FontWeight.Medium,
215 | fontFamily = poppins,
216 | color = Color.White)
217 |
218 | }
219 | Row(modifier = Modifier.padding(top = 2.dp)){
220 | Text(text ="Discounted cash flow : ",
221 | modifier = Modifier
222 | .padding(start = 12.dp),
223 | fontSize = 15.sp,
224 | fontWeight = FontWeight.Medium,
225 | fontFamily = poppins,
226 | color = Color.LightGray)
227 |
228 | Text(text =state.companyDetail.dcf.toString(),
229 | modifier = Modifier,
230 | fontSize = 15.sp,
231 | fontWeight = FontWeight.Medium,
232 | fontFamily = poppins,
233 | color = Color.White)
234 |
235 | }
236 | Row(modifier = Modifier.padding(top = 2.dp)){
237 | Text(text ="Earning Per Share: ",
238 | modifier = Modifier
239 | .padding(start = 12.dp),
240 | fontSize = 15.sp,
241 | fontWeight = FontWeight.Medium,
242 | fontFamily = poppins,
243 | color = Color.LightGray)
244 |
245 | Text(text =quote.eps.toString(),
246 | modifier = Modifier,
247 | fontSize = 15.sp,
248 | fontWeight = FontWeight.Medium,
249 | fontFamily = poppins,
250 | color = Color.White)
251 |
252 | }
253 | }
254 | }
255 |
256 | }
257 | }
258 |
259 | }
260 | }
261 |
262 | if( state.isLoading || state.error!=null){
263 |
264 | Box(modifier = Modifier
265 | .fillMaxSize()
266 | .background(color = Color.Black), contentAlignment = Alignment.Center){
267 | if(state.isLoading){
268 | CircularProgressIndicator(color = Orange)
269 | }
270 |
271 | else {
272 | Text(text = state.error ?: "",
273 | color = Color.White,
274 | fontSize = 15.sp,
275 | modifier = Modifier.padding(start = 12.dp, end = 12.dp))
276 | }
277 | }
278 | }
279 |
280 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/investiq/presentation/company_list/CompanyListScreen.kt:
--------------------------------------------------------------------------------
1 | package com.example.investiq.presentation.company_list
2 |
3 | import androidx.compose.animation.core.EaseOut
4 | import androidx.compose.animation.core.animateDpAsState
5 | import androidx.compose.animation.core.animateFloatAsState
6 | import androidx.compose.animation.core.animateIntAsState
7 | import androidx.compose.animation.core.tween
8 | import androidx.compose.foundation.background
9 | import androidx.compose.foundation.border
10 | import androidx.compose.foundation.layout.Arrangement
11 | import androidx.compose.foundation.layout.Box
12 | import androidx.compose.foundation.layout.Column
13 | import androidx.compose.foundation.layout.Spacer
14 | import androidx.compose.foundation.layout.fillMaxSize
15 | import androidx.compose.foundation.layout.fillMaxWidth
16 | import androidx.compose.foundation.layout.height
17 | import androidx.compose.foundation.layout.offset
18 | import androidx.compose.foundation.layout.padding
19 | import androidx.compose.foundation.layout.size
20 | import androidx.compose.foundation.layout.wrapContentHeight
21 | import androidx.compose.foundation.layout.wrapContentSize
22 | import androidx.compose.foundation.layout.wrapContentWidth
23 | import androidx.compose.foundation.lazy.LazyColumn
24 | import androidx.compose.foundation.lazy.itemsIndexed
25 | import androidx.compose.foundation.shape.CircleShape
26 | import androidx.compose.foundation.shape.RoundedCornerShape
27 | import androidx.compose.material.CircularProgressIndicator
28 | import androidx.compose.material.ExperimentalMaterialApi
29 | import androidx.compose.material.OutlinedTextField
30 | import androidx.compose.material.Text
31 | import androidx.compose.material.TextFieldDefaults
32 | import androidx.compose.material.pullrefresh.PullRefreshState
33 | import androidx.compose.material.pullrefresh.pullRefresh
34 | import androidx.compose.material.pullrefresh.rememberPullRefreshState
35 | import androidx.compose.material3.MaterialTheme
36 | import androidx.compose.runtime.Composable
37 | import androidx.compose.runtime.LaunchedEffect
38 | import androidx.compose.runtime.derivedStateOf
39 | import androidx.compose.runtime.getValue
40 | import androidx.compose.runtime.mutableStateOf
41 | import androidx.compose.runtime.remember
42 | import androidx.compose.runtime.setValue
43 | import androidx.compose.ui.Alignment
44 | import androidx.compose.ui.Modifier
45 | import androidx.compose.ui.draw.drawWithCache
46 | import androidx.compose.ui.focus.onFocusChanged
47 | import androidx.compose.ui.geometry.Rect
48 | import androidx.compose.ui.graphics.Color
49 | import androidx.compose.ui.graphics.Outline
50 | import androidx.compose.ui.graphics.Path
51 | import androidx.compose.ui.graphics.PathMeasure
52 | import androidx.compose.ui.graphics.StrokeCap
53 | import androidx.compose.ui.graphics.drawOutline
54 | import androidx.compose.ui.graphics.drawscope.DrawScope
55 | import androidx.compose.ui.graphics.drawscope.Stroke
56 | import androidx.compose.ui.graphics.graphicsLayer
57 | import androidx.compose.ui.hapticfeedback.HapticFeedbackType
58 | import androidx.compose.ui.platform.LocalHapticFeedback
59 | import androidx.compose.ui.text.buildAnnotatedString
60 | import androidx.compose.ui.text.font.FontWeight
61 | import androidx.compose.ui.text.style.TextAlign
62 | import androidx.compose.ui.unit.IntOffset
63 | import androidx.compose.ui.unit.dp
64 | import androidx.compose.ui.unit.sp
65 | import androidx.compose.ui.zIndex
66 | import androidx.hilt.navigation.compose.hiltViewModel
67 | import androidx.navigation.NavController
68 | import com.airbnb.lottie.compose.LottieAnimation
69 | import com.airbnb.lottie.compose.LottieCompositionSpec
70 | import com.airbnb.lottie.compose.LottieConstants
71 | import com.airbnb.lottie.compose.rememberLottieComposition
72 | import com.example.investiq.R
73 | import com.example.investiq.presentation.company_favorites.CompanyFavoriteViewModel
74 | import com.example.investiq.presentation.screens.Screen
75 | import com.example.investiq.ui.theme.Orange
76 | import com.example.investiq.ui.theme.PurpleGrey40
77 | import com.example.investiq.ui.theme.poppins
78 | import kotlinx.coroutines.delay
79 | import kotlin.math.roundToInt
80 |
81 | @OptIn( ExperimentalMaterialApi::class)
82 | @Composable
83 | fun CompanyListScreen(
84 | navigator: NavController,
85 | companyListingViewmodel:CompanyListingViewmodel = hiltViewModel(),
86 | companyFavoriteViewModel: CompanyFavoriteViewModel= hiltViewModel()
87 | ){
88 |
89 | var clickedSearch by remember {
90 | mutableStateOf(false)
91 | }
92 |
93 | val progress by animateFloatAsState(targetValue = if(clickedSearch) 1f else 0f, label = "", animationSpec = tween(2000))
94 |
95 | val state = companyListingViewmodel.state
96 |
97 | val pullRefreshState = rememberPullRefreshState(
98 | refreshing = companyListingViewmodel.state.isRefreshing ,
99 | onRefresh = {
100 | companyListingViewmodel.getCompanyListing(fetchFromRemote = true)
101 | })
102 |
103 | val willRefresh by remember {
104 | derivedStateOf {
105 | pullRefreshState.progress > 1f
106 | }
107 | }
108 |
109 | val cardOffset by animateIntAsState(
110 | targetValue = when{
111 | state.isRefreshing -> 250
112 | pullRefreshState.progress in 0f..1f -> (250*pullRefreshState.progress).roundToInt()
113 | pullRefreshState.progress > 1f -> (250 + ((pullRefreshState.progress - 1f) * .1f) * 100).roundToInt()
114 | else -> 0
115 | },
116 | label = "cardOffset" )
117 |
118 | val cardRotation by animateFloatAsState(
119 | targetValue = when{
120 | state.isRefreshing || pullRefreshState.progress>1f -> 5f
121 | pullRefreshState.progress > 0f -> 5 * pullRefreshState.progress
122 | else -> 0f
123 | } ,
124 | label = "cardRotation" )
125 |
126 | // vibration on pull
127 | val hapticFeedback = LocalHapticFeedback.current
128 | LaunchedEffect(key1 = willRefresh) {
129 | when{
130 | willRefresh->{
131 | hapticFeedback.performHapticFeedback(HapticFeedbackType.TextHandleMove)
132 | delay(70)
133 | hapticFeedback.performHapticFeedback(HapticFeedbackType.TextHandleMove)
134 | delay(100)
135 | hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
136 | }
137 |
138 | !companyListingViewmodel.state.isRefreshing && pullRefreshState.progress > 0f -> {
139 | hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
140 | }
141 | }
142 | }
143 | // --
144 |
145 |
146 | // animating orange background
147 | val animatedOffset by animateDpAsState(
148 | targetValue = when {
149 | state.isRefreshing -> 200.dp
150 | pullRefreshState.progress in 0f..1f -> (pullRefreshState.progress * 200).dp
151 | pullRefreshState.progress > 1f -> (200 + (((pullRefreshState.progress - 1f) * .1f) * 200)).dp
152 | else -> 0.dp
153 | }, label = ""
154 | )
155 |
156 | val isBackgroundVisible = pullRefreshState.progress > 0f
157 |
158 | if (isBackgroundVisible) {
159 | Box(
160 | modifier = Modifier
161 | .fillMaxSize()
162 | .background(Orange) // Orange color
163 | .graphicsLayer {
164 | translationY = animatedOffset.toPx()
165 | }
166 | )
167 | }
168 | // --
169 |
170 | Box(modifier= Modifier
171 | .fillMaxSize()
172 | .pullRefresh(pullRefreshState)
173 | ) {
174 |
175 | Column(modifier=Modifier.fillMaxSize()){
176 |
177 | Box(modifier = Modifier
178 | .fillMaxWidth()
179 | .height(80.dp)){
180 |
181 | if (pullRefreshState.progress==0f) {
182 |
183 | OutlinedTextField( value = state.searchQuery ,
184 | onValueChange = {value->
185 | companyListingViewmodel.onEvent(CompanyListingEvent.OnSearchQueryChanged(value))
186 | },
187 | modifier = Modifier
188 | .padding(
189 | bottom = 15.dp,
190 | start = 16.dp,
191 | end = 16.dp,
192 | top = 10.dp
193 | )
194 | .fillMaxWidth()
195 | .onFocusChanged { focusState ->
196 | clickedSearch = focusState.isFocused
197 | }
198 | .animatedBorder({ progress }, Orange, Color.Black),
199 | maxLines = 1,
200 | placeholder = { Text(text = "Search...")}
201 | , colors = TextFieldDefaults.outlinedTextFieldColors(
202 | focusedBorderColor =Color.Transparent,
203 | unfocusedBorderColor = PurpleGrey40 ,
204 | placeholderColor = Color.Black,
205 | disabledPlaceholderColor = Color.Yellow),
206 | shape = MaterialTheme.shapes.extraLarge
207 | )
208 |
209 | }
210 |
211 | }
212 |
213 | LazyColumn(modifier=Modifier.fillMaxSize()) {
214 | itemsIndexed(state.companyList){ index, companyItem ->
215 |
216 | CompanyItem(
217 | Modifier
218 | .zIndex((state.companyList.size - index).toFloat())
219 | .graphicsLayer {
220 | rotationZ = cardRotation * if (index % 2 == 0) 1 else -1
221 | translationY = (cardOffset * ((5f - (index + 1)) / 5f)).dp
222 | .roundToPx()
223 | .toFloat()
224 | },
225 | company = companyItem,
226 | onClick = { navigator.navigate(Screen.FavoritesScreen.route+"/${companyItem.symbol}") } ,
227 | viewmodel = companyFavoriteViewModel
228 | )
229 | }
230 | }
231 | }
232 |
233 | if(state.error.isNotEmpty() && state.companyList.isEmpty()){
234 |
235 | Box(modifier= Modifier
236 | .padding(12.dp)
237 | .border(width = 2.dp, color = Color.Black, shape = RoundedCornerShape(5.dp))
238 | .wrapContentWidth()
239 | .wrapContentHeight()
240 | .padding(10.dp)
241 | .align(Alignment.Center)
242 | , contentAlignment = Alignment.Center
243 | ){
244 | Text(text = state.error,
245 | color = Color.Black,
246 | fontSize = 15.sp
247 | )
248 | }
249 |
250 | }
251 |
252 | if (state.isLoading){
253 | Box(modifier = Modifier
254 | .wrapContentSize()
255 | .background(color = Color.Transparent)
256 | .align(Alignment.Center)){
257 | CircularProgressIndicator(color = Orange)
258 | }
259 | }
260 |
261 | CustomIndicator(companyListingViewmodel.state.isRefreshing,pullRefreshState)
262 |
263 | }
264 | }
265 |
266 | @OptIn(ExperimentalMaterialApi::class)
267 | @Composable
268 | fun CustomIndicator(isRefreshing:Boolean,pullRefreshState:PullRefreshState){
269 |
270 | // handling animation
271 |
272 | val animatedOffset by animateDpAsState(
273 | targetValue = when {
274 | isRefreshing -> 200.dp
275 | pullRefreshState.progress in 0f..1f -> (pullRefreshState.progress * 200).dp
276 | pullRefreshState.progress > 1f -> (200 + (((pullRefreshState.progress - 1f) * .1f) * 200)).dp
277 | else -> 0.dp
278 | }, label = ""
279 | )
280 |
281 | val composition by rememberLottieComposition(spec = LottieCompositionSpec.RawRes(resId = R.raw.anim3))
282 |
283 | val isAnimationVisible by remember {
284 | derivedStateOf {
285 | animatedOffset>0.dp
286 | }
287 | }
288 |
289 | Box(
290 | modifier = Modifier
291 | .fillMaxWidth()
292 | .height(150.dp)
293 | .offset(y = (-200).dp)
294 | .offset { IntOffset(0, animatedOffset.roundToPx()) }
295 | ){
296 |
297 | // Check if animation should be visible
298 | if (isAnimationVisible) {
299 | // Centered Column for animation and loading text
300 | Column(
301 | modifier = Modifier.fillMaxSize(),
302 | verticalArrangement = Arrangement.Center,
303 | horizontalAlignment = Alignment.CenterHorizontally
304 | ) {
305 | // LottieAnimation
306 | LottieAnimation(
307 | composition = composition,
308 | iterations = LottieConstants.IterateForever,
309 | modifier = Modifier.size(100.dp)
310 | )
311 | // Spacer to create space between animation and text
312 | Spacer(modifier = Modifier.height(2.dp))
313 | // AnimatedLoadingText
314 | AnimatedLoadingText(pullRefreshState)
315 | }
316 | }
317 | }
318 | }
319 |
320 | @OptIn(ExperimentalMaterialApi::class)
321 | @Composable
322 | fun AnimatedLoadingText(pullRefreshState:PullRefreshState) {
323 | var dotCount by remember { mutableStateOf(0) }
324 |
325 | LaunchedEffect(Unit) {
326 | while (true) {
327 | delay(500) // Adjust delay time as needed
328 | dotCount = (dotCount + 1) % 4 // Update dot count
329 | }
330 | }
331 |
332 | if (pullRefreshState.progress in 0f..1f){
333 | Text(
334 | buildAnnotatedString {
335 | append("Pull to Refresh ")
336 | repeat(dotCount) {
337 | append(".")
338 | }
339 | },
340 | modifier = Modifier.padding(top = 8.dp),
341 | textAlign = TextAlign.Center,
342 | fontFamily = poppins,
343 | fontWeight = FontWeight.SemiBold,
344 | color = Color.White,
345 | fontSize = 15.sp,
346 | )
347 | }
348 |
349 | if(pullRefreshState.progress>=1f){
350 | Text(
351 | buildAnnotatedString {
352 | append("• Release •")
353 | },
354 | modifier = Modifier.padding(top = 8.dp),
355 | textAlign = TextAlign.Center,
356 | fontFamily = poppins,
357 | fontWeight = FontWeight.SemiBold,
358 | color = Color.White,
359 | fontSize = 15.sp,
360 | )
361 | }
362 |
363 | }
364 |
365 | fun Modifier.animatedBorder
366 | (provideProgress: () -> Float,
367 | colorFocused: Color,
368 | colorUnfocused: Color) = this.drawWithCache {
369 | val width = size.width
370 | val height = size.height
371 |
372 | val shape = CircleShape
373 |
374 | // Only works with RoundedCornerShape...
375 | val outline = shape.createOutline(size, layoutDirection, this) as Outline.Rounded
376 |
377 | // ... correction: Only works with same corner sizes everywhere
378 | val radius = outline.roundRect.topLeftCornerRadius.x
379 | val diameter = 2 * radius
380 |
381 | // Clockwise path
382 | val pathCw = Path()
383 |
384 | // Start top center
385 | pathCw.moveTo(width / 2, 0f)
386 |
387 | // Line to right
388 | pathCw.lineTo(width - radius, 0f)
389 |
390 | // Top right corner
391 | pathCw.arcTo(Rect(width - diameter, 0f, width, diameter), -90f, 90f, false)
392 |
393 | // Right edge
394 | pathCw.lineTo(width, height - radius)
395 |
396 | // Bottom right corner
397 | pathCw.arcTo(Rect(width - diameter, height - diameter, width, height), 0f, 90f, false)
398 |
399 | // Line to bottom center
400 | pathCw.lineTo(width / 2, height)
401 |
402 | // As above, but mirrored horizontally
403 | val pathCcw = Path()
404 | pathCcw.moveTo(width / 2, 0f)
405 | pathCcw.lineTo(radius, 0f)
406 | pathCcw.arcTo(Rect(0f, 0f, diameter, diameter), -90f, -90f, false)
407 | pathCcw.lineTo(0f, height - radius)
408 | pathCcw.arcTo(Rect(0f, height - diameter, diameter, height), 180f, -90f, false)
409 | pathCcw.lineTo(width / 2, height)
410 |
411 | val pmCw = PathMeasure().apply {
412 | setPath(pathCw, false)
413 | }
414 | val pmCcw = PathMeasure().apply {
415 | setPath(pathCcw, false)
416 | }
417 |
418 | fun DrawScope.drawIndicator(progress: Float, pathMeasure: PathMeasure) {
419 | val subPath = Path()
420 | pathMeasure.getSegment(0f, pathMeasure.length * EaseOut.transform(progress), subPath)
421 | drawPath(subPath, colorFocused, style = Stroke(3.dp.toPx(), cap = StrokeCap.Round))
422 | }
423 |
424 | onDrawBehind {
425 | // Draw the shape
426 | drawOutline(outline, colorUnfocused, style = Stroke(2.dp.toPx()))
427 |
428 | // Draw the indicators
429 | drawIndicator(provideProgress(), pmCw)
430 | drawIndicator(provideProgress(), pmCcw)
431 | }
432 | }
433 |
--------------------------------------------------------------------------------
/app/src/main/res/raw/anim3.json:
--------------------------------------------------------------------------------
1 | {"nm":"Rabbit R1 Rabbit","ddd":0,"h":500,"w":500,"meta":{"g":"@lottiefiles/creator 1.12.0"},"layers":[{"ty":4,"nm":"Layer_4","sr":1,"st":42,"op":52,"ip":42,"hd":false,"ln":"Layer_4","ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[36.249999999999986,150.23674774169922]},"s":{"a":0,"k":[202.55267952915443,202.55267952915443]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0.55,"y":0.06},"i":{"x":1,"y":1},"s":[161.6395,354.6187],"t":0},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[161.6395,372.8421],"t":18},{"o":{"x":0.65,"y":0},"i":{"x":1,"y":1},"s":[161.6395,354.6187],"t":36},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[161.6395,372.8421],"t":54},{"o":{"x":0.65,"y":0},"i":{"x":1,"y":1},"s":[161.6395,354.6187],"t":72},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[161.6395,372.8421],"t":90},{"o":{"x":0.65,"y":0},"i":{"x":1,"y":1},"s":[161.6395,354.6187],"t":108},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[161.6395,372.8421],"t":126},{"s":[161.6395,354.6187],"t":144}]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"shapes":[{"ty":"gr","bm":0,"hd":false,"nm":"Group 2","it":[{"ty":"sh","bm":0,"hd":false,"nm":"Path 2","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[-0.30000000000000426,-0.09999999999999432],[0,0],[-7.5,3.6999999999999886],[0,0],[0,-0.30000000000001137],[0,0],[0.20000000000000284,0],[0,0],[7.600000000000001,3.6000000000000227],[0,0],[0,0.19999999999998863],[0,0]],"o":[[0,-0.4000000000000057],[0,0],[7.600000000000001,3.5999999999999943],[0,0],[0.29999999999999716,-0.19999999999998863],[0,0],[0,0.19999999999998863],[0,0],[-7.5,3.6999999999999886],[0,0],[-0.20000000000000284,-0.09999999999999432],[0,0],[0,0]],"v":[[43.5,159.3],[44.2,158.8],[45.800000000000004,159.5],[69.7,159.3],[70.8,158.70000000000002],[71.5,159.10000000000002],[71.5,161.50000000000003],[71.2,161.90000000000003],[69.60000000000001,162.70000000000005],[45.70000000000001,162.90000000000003],[43.70000000000001,162.00000000000003],[43.40000000000001,161.50000000000003],[43.40000000000001,159.3]]}}},{"ty":"fl","bm":0,"hd":false,"nm":"Fill","c":{"a":0,"k":[0,0,0,1]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","a":{"a":0,"k":[57.450000762939446,162.08674621582028]},"s":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[87,47],"t":42},{"s":[100,100],"t":44}]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[57.450000762939446,162.08674621582028]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}}]},{"ty":"gr","bm":0,"hd":false,"nm":"Group 3","it":[{"ty":"sh","bm":0,"hd":false,"nm":"Path 3","d":1,"ks":{"a":0,"k":{"c":true,"i":[[4.2,0],[0,0],[-0.19999999999999996,0],[-3.6999999999999993,1.8000000000000114],[0,0],[0,-0.30000000000001137],[0,0],[0.20000000000000107,0],[0,0]],"o":[[0,0],[0.19999999999999996,0],[4,-0.09999999999999432],[0,0],[0.3000000000000007,-0.19999999999998863],[0,0],[0,0.19999999999998863],[0,0],[-3.799999999999997,1.9999999999999716]],"v":[[1,141.89999999999998],[1,138.39999999999998],[1.499999999999993,138.39999999999998],[13.200000000000003,135.6],[14.299999999999997,135],[14.999999999999993,135.4],[14.999999999999993,137.79999999999998],[14.699999999999989,138.20000000000002],[13.099999999999994,139.00000000000003]]}}},{"ty":"fl","bm":0,"hd":false,"nm":"Fill","c":{"a":0,"k":[0,0,0,1]},"r":2,"o":{"a":0,"k":100}},{"ty":"tr","a":{"a":0,"k":[8,138.41799926757812]},"s":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[87,47],"t":42},{"s":[100,100],"t":44}]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[8,138.41799926757812]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}}]}],"ind":1},{"ty":4,"nm":"Layer_2","sr":1,"st":0,"op":150,"ip":0,"hd":false,"ln":"Layer_2","ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[107,124.57199859619138]},"s":{"a":0,"k":[202.55267952915443,202.55267952915443]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0.55,"y":0.06},"i":{"x":1,"y":1},"s":[304.9456,302.634],"t":0},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[304.9456,320.8574],"t":18},{"o":{"x":0.55,"y":0.06},"i":{"x":1,"y":1},"s":[304.9456,302.634],"t":36},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[304.9456,320.8574],"t":54},{"o":{"x":0.55,"y":0.06},"i":{"x":1,"y":1},"s":[304.9456,302.634],"t":72},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[304.9456,320.8574],"t":90},{"o":{"x":0.55,"y":0.06},"i":{"x":1,"y":1},"s":[304.9456,302.634],"t":108},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[304.9456,320.8574],"t":126},{"s":[304.9456,302.634],"t":144}]},"r":{"a":1,"k":[{"o":{"x":0.55,"y":0.06},"i":{"x":1,"y":1},"s":[0],"t":0},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[30],"t":14},{"o":{"x":0.55,"y":0.06},"i":{"x":1,"y":1},"s":[0],"t":32},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[30],"t":50},{"o":{"x":0.55,"y":0.06},"i":{"x":1,"y":1},"s":[0],"t":68},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[30],"t":86},{"o":{"x":0.55,"y":0.06},"i":{"x":1,"y":1},"s":[0],"t":104},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[30],"t":122},{"s":[0],"t":140}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"shapes":[{"ty":"gr","bm":0,"hd":false,"nm":"Group 1","it":[{"ty":"sh","bm":0,"hd":false,"nm":"Path 1","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,-3.1000000000000014],[0,0],[0.6999999999999886,-0.5999999999999943],[0,0],[0,3.0999999999999943],[0,0],[-0.6999999999999886,0.5999999999999943]],"o":[[0,0],[2.1999999999999886,-2.1000000000000014],[0,0],[0,1],[0,0],[-2.200000000000003,2.0999999999999943],[0,0],[0,-1],[0,0]],"v":[[109.6,75.9],[152.1,35.2],[158,37.7],[158,78.9],[156.9,81.4],[114.4,122.10000000000001],[108.5,119.60000000000001],[108.5,78.4],[109.6,75.9]]}}},{"ty":"st","bm":0,"hd":false,"nm":"Stroke","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":0,"k":3},"c":{"a":0,"k":[0,0,0,1]}},{"ty":"fl","bm":0,"hd":false,"nm":"Fill","c":{"a":0,"k":[1,1,1,1]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","a":{"a":0,"k":[106.99999999999997,124.5719985961914]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[106.99999999999997,124.5719985961914]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}}]}],"ind":2},{"ty":4,"nm":"Layer_5","sr":1,"st":0,"op":42,"ip":0,"hd":false,"ln":"Layer_5","ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[202.55267952915443,202.55267952915443]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0.55,"y":0.06},"i":{"x":1,"y":1},"s":[88.2142,50.3101],"t":0},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[88.2142,68.5335],"t":18},{"o":{"x":0.65,"y":0},"i":{"x":1,"y":1},"s":[88.2142,50.3101],"t":36},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[88.2142,68.5335],"t":54},{"o":{"x":0.65,"y":0},"i":{"x":1,"y":1},"s":[88.2142,50.3101],"t":72},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[88.2142,68.5335],"t":90},{"o":{"x":0.65,"y":0},"i":{"x":1,"y":1},"s":[88.2142,50.3101],"t":108},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[88.2142,68.5335],"t":126},{"s":[88.2142,50.3101],"t":144}]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"shapes":[{"ty":"gr","bm":0,"hd":false,"nm":"Shape Layer 1","it":[{"ty":"gr","bm":0,"hd":false,"nm":"Group 4","it":[{"ty":"sh","bm":0,"hd":false,"nm":"Path 4","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,-0.8999999999999773],[0.5999999999999996,-0.5999999999999943],[0,0]],"o":[[0,1],[0,0],[0.5999999999999996,0.8000000000000114]],"v":[[4.999999999999993,140.19999999999996],[3.999999999999993,142.69999999999996],[3.999999999999993,137.59999999999997]]}}},{"ty":"st","bm":0,"hd":false,"nm":"Stroke","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":0,"k":3},"c":{"a":0,"k":[0,0,0,1]}},{"ty":"fl","bm":0,"hd":false,"nm":"Fill","c":{"a":0,"k":[0.02,0.02,0.02,1]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","a":{"a":0,"k":[4.5,140.15000152587885]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[4.5,140.15000152587885]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}}]},{"ty":"gr","bm":0,"hd":false,"nm":"Group 5","it":[{"ty":"el","bm":0,"hd":false,"nm":"Ellipse 1","d":1,"p":{"a":0,"k":[57.8,164.2]},"s":{"a":0,"k":[7.6,7.6]}},{"ty":"st","bm":0,"hd":false,"nm":"Stroke","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":0,"k":3},"c":{"a":0,"k":[0,0,0,1]}},{"ty":"fl","bm":0,"hd":false,"nm":"Fill","c":{"a":0,"k":[0.02,0.02,0.02,1]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}}]},{"ty":"tr","a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}}]}],"ind":3},{"ty":4,"nm":"Layer_5","sr":1,"st":52,"op":144,"ip":52,"hd":false,"ln":"Layer_5","ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[202.55267952915443,202.55267952915443]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0.55,"y":0.06},"i":{"x":1,"y":1},"s":[88.2142,50.3101],"t":1},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[88.2142,68.5335],"t":19},{"o":{"x":0.65,"y":0},"i":{"x":1,"y":1},"s":[88.2142,50.3101],"t":37},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[88.2142,68.5335],"t":55},{"o":{"x":0.65,"y":0},"i":{"x":1,"y":1},"s":[88.2142,50.3101],"t":73},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[88.2142,68.5335],"t":91},{"o":{"x":0.65,"y":0},"i":{"x":1,"y":1},"s":[88.2142,50.3101],"t":109},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[88.2142,68.5335],"t":127},{"s":[88.2142,50.3101],"t":145}]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"shapes":[{"ty":"gr","bm":0,"hd":false,"nm":"Shape Layer 1","it":[{"ty":"gr","bm":0,"hd":false,"nm":"Group 4","it":[{"ty":"sh","bm":0,"hd":false,"nm":"Path 4","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0.5999999999999996,-0.5999999999999943],[0,0],[0,-0.8999999999999773]],"o":[[0,1],[0,0],[0.5999999999999996,0.8000000000000114],[0,0]],"v":[[5,140.2],[4,142.7],[4,137.6],[5,140.2]]}}},{"ty":"st","bm":0,"hd":false,"nm":"Stroke","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":0,"k":3},"c":{"a":0,"k":[0,0,0,1]}},{"ty":"fl","bm":0,"hd":false,"nm":"Fill","c":{"a":0,"k":[0.02,0.02,0.02,1]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}}]},{"ty":"gr","bm":0,"hd":false,"nm":"Group 5","it":[{"ty":"el","bm":0,"hd":false,"nm":"Ellipse 1","d":1,"p":{"a":0,"k":[57.8,164.2]},"s":{"a":0,"k":[7.6,7.6]}},{"ty":"st","bm":0,"hd":false,"nm":"Stroke","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":0,"k":3},"c":{"a":0,"k":[0,0,0,1]}},{"ty":"fl","bm":0,"hd":false,"nm":"Fill","c":{"a":0,"k":[0.02,0.02,0.02,1]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}}]},{"ty":"tr","a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}}]}],"ind":4},{"ty":4,"nm":"Layer_6","sr":1,"st":0,"op":150,"ip":0,"hd":false,"ln":"Layer_6","ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[25.150000572204576,161.44999694824216]},"s":{"a":0,"k":[202.55267952915443,202.55267952915443]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0.55,"y":0.06},"i":{"x":1,"y":1},"s":[139.1562,377.3314],"t":0},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[139.1562,395.5548],"t":18},{"o":{"x":0.65,"y":0},"i":{"x":1,"y":1},"s":[139.1562,377.3314],"t":36},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[139.1562,395.5548],"t":54},{"o":{"x":0.65,"y":0},"i":{"x":1,"y":1},"s":[139.1562,377.3314],"t":72},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[139.1562,395.5548],"t":90},{"o":{"x":0.65,"y":0},"i":{"x":1,"y":1},"s":[139.1562,377.3314],"t":108},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[139.1562,395.5548],"t":126},{"s":[139.1562,377.3314],"t":144}]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"shapes":[{"ty":"gr","bm":0,"hd":false,"nm":"Group 6","it":[{"ty":"sh","bm":0,"hd":false,"nm":"Path 5","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0.09999999999999787,0.09999999999999432],[0,0],[0,-0.4000000000000057],[0,0],[0.40000000000000213,0.19999999999998863],[0,0],[0,-0.4000000000000057],[0,0],[-0.09999999999999964,-0.09999999999999432],[0,0],[0,-0.09999999999999432],[0,0],[-0.09999999999999787,-0.09999999999999432],[0,0],[0,0.4000000000000057],[0,0],[-0.40000000000000213,-0.19999999999998863],[0,0],[0,0.4000000000000057],[0,0],[0.10000000000000142,0.09999999999999432],[0,0],[0,0.09999999999999432],[0,0]],"o":[[0,-0.19999999999998863],[0,0],[-0.3000000000000007,-0.19999999999998863],[0,0],[0,0.4000000000000057],[0,0],[-0.3000000000000007,-0.19999999999998863],[0,0],[0,0.19999999999998863],[0,0],[0.1999999999999993,0.09999999999999432],[0,0],[0,0.19999999999998863],[0,0],[0.3000000000000007,0.19999999999998863],[0,0],[0,-0.4000000000000057],[0,0],[0.29999999999999716,0.19999999999998863],[0,0],[0,-0.19999999999998863],[0,0],[-0.1999999999999993,-0.09999999999999432],[0,0],[0,0]],"v":[[27,152.8],[26.8,152.4],[24.3,150.6],[23.5,151],[23.5,157.6],[22.7,158],[15.7,153.9],[14.899999999999999,154.3],[14.899999999999999,156.60000000000002],[15.099999999999998,157.00000000000003],[23.099999999999998,161.70000000000002],[23.299999999999997,162.10000000000002],[23.299999999999997,170.10000000000002],[23.499999999999996,170.50000000000003],[25.999999999999996,172.30000000000004],[26.799999999999997,171.90000000000003],[26.799999999999997,164.80000000000004],[27.599999999999998,164.40000000000003],[34.599999999999994,168.50000000000003],[35.39999999999999,168.10000000000002],[35.39999999999999,165.8],[35.19999999999999,165.4],[27.19999999999999,160.70000000000002],[26.99999999999999,160.3],[26.99999999999999,152.8]]}}},{"ty":"fl","bm":0,"hd":false,"nm":"Fill","c":{"a":0,"k":[0.02,0.02,0.02,1]},"r":2,"o":{"a":0,"k":100}},{"ty":"tr","a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}}]}],"ind":5},{"ty":4,"nm":"Layer_1","sr":1,"st":0,"op":150,"ip":0,"hd":false,"ln":"Layer_1","ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[65.15000152587889,139.39999389648435]},"s":{"a":0,"k":[202.55267952915443,202.5527]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0.55,"y":0.06},"i":{"x":1,"y":1},"s":[220.1773,332.6685],"t":0},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[220.1773,350.8919],"t":18},{"o":{"x":0.65,"y":0},"i":{"x":1,"y":1},"s":[220.1773,332.6685],"t":36},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[220.1773,350.8919],"t":54},{"o":{"x":0.65,"y":0},"i":{"x":1,"y":1},"s":[220.1773,332.6685],"t":72},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[220.1773,350.8919],"t":90},{"o":{"x":0.65,"y":0},"i":{"x":1,"y":1},"s":[220.1773,332.6685],"t":108},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[220.1773,350.8919],"t":126},{"s":[220.1773,332.6685],"t":144}]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"shapes":[{"ty":"gr","bm":0,"hd":false,"nm":"Group 7","it":[{"ty":"sh","bm":0,"hd":false,"nm":"Path 6","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,-6.900000000000006],[0,0],[4,-2.1999999999999886],[0,0],[15,8.199999999999989],[0,0],[0,4.599999999999994],[0,0],[-6.1,3.0999999999999943],[0,0],[-14.399999999999991,-7.400000000000006]],"o":[[0,0],[6.200000000000003,3.200000000000003],[0,0],[0,4.599999999999994],[0,0],[-14.900000000000006,8.199999999999989],[0,0],[-4,-2.1999999999999886],[0,0],[0,-6.900000000000006],[0,0],[14.399999999999999,-7.400000000000006],[0,0]],"v":[[88.1,92],[118.8,107.9],[128.8,124.30000000000001],[128.8,157.10000000000002],[122.30000000000001,168.10000000000002],[89.10000000000001,186.20000000000002],[41.10000000000001,186.20000000000002],[8,168],[1.5,157],[1.5,124.2],[11.5,107.80000000000001],[42.2,92],[88.1,92]]}}},{"ty":"st","bm":0,"hd":false,"nm":"Stroke","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":0,"k":3},"c":{"a":0,"k":[0,0,0,1]}},{"ty":"fl","bm":0,"hd":false,"nm":"Fill","c":{"a":0,"k":[1,1,1,1]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}}]}],"ind":6},{"ty":4,"nm":"Layer_3","sr":1,"st":0,"op":150,"ip":0,"hd":false,"ln":"Layer_3","ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[61.081897735595696,101.68240356445312]},"s":{"a":0,"k":[202.55267952915443,202.55267952915443]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0.55,"y":0.06},"i":{"x":1,"y":1},"s":[211.9372,256.2705],"t":0},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[211.9372,274.4939],"t":18},{"o":{"x":0.65,"y":0},"i":{"x":1,"y":1},"s":[211.9372,256.2705],"t":36},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[211.9372,274.4939],"t":54},{"o":{"x":0.65,"y":0},"i":{"x":1,"y":1},"s":[211.9372,256.2705],"t":72},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[211.9372,274.4939],"t":90},{"o":{"x":0.65,"y":0},"i":{"x":1,"y":1},"s":[211.9372,256.2705],"t":108},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[211.9372,274.4939],"t":126},{"s":[211.9372,256.2705],"t":144}]},"r":{"a":1,"k":[{"o":{"x":0.55,"y":0.06},"i":{"x":1,"y":1},"s":[0],"t":0},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[30],"t":16},{"o":{"x":0.55,"y":0.06},"i":{"x":1,"y":1},"s":[0],"t":34},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[30],"t":52},{"o":{"x":0.55,"y":0.06},"i":{"x":1,"y":1},"s":[0],"t":70},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[30],"t":88},{"o":{"x":0.55,"y":0.06},"i":{"x":1,"y":1},"s":[0],"t":106},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[30],"t":124},{"s":[0],"t":142}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"shapes":[{"ty":"gr","bm":0,"hd":false,"nm":"Group 8","it":[{"ty":"sh","bm":0,"hd":false,"nm":"Path 7","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[-0.29999999999999716,-3.0999999999999996],[0,0],[0.5999999999999943,-0.7000000000000028],[0,0],[0.29999999999999716,3.0999999999999943],[0,0],[-0.6000000000000014,0.8000000000000043]],"o":[[0,0],[2,-2.3],[0,0],[0.09999999999999432,1],[0,0],[-2,2.299999999999997],[0,0],[-0.10000000000000853,-0.8999999999999986],[0,0]],"v":[[63.4,53.4],[102.1,9.100000000000001],[108.19999999999999,11.100000000000001],[111.89999999999999,52.1],[111.1,54.7],[72.4,99],[66.30000000000001,97],[62.60000000000001,56],[63.4,53.4]]}}},{"ty":"st","bm":0,"hd":false,"nm":"Stroke","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":0,"k":3},"c":{"a":0,"k":[0,0,0,1]}},{"ty":"fl","bm":0,"hd":false,"nm":"Fill","c":{"a":0,"k":[1,1,1,1]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}}]}],"ind":7}],"v":"5.7.0","fr":30,"op":144,"ip":0,"assets":[]}
--------------------------------------------------------------------------------
/app/src/main/res/raw/anim2.json:
--------------------------------------------------------------------------------
1 | {"nm":"Rabbit R1 Rabbit","ddd":0,"h":500,"w":500,"meta":{"g":"@lottiefiles/creator 1.12.0"},"layers":[{"ty":4,"nm":"Layer_4","sr":1,"st":42,"op":52,"ip":42,"hd":false,"ln":"Layer_4","ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[36.249999999999986,150.23674774169922]},"s":{"a":0,"k":[202.55267952915443,202.55267952915443]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0.55,"y":0.06},"i":{"x":1,"y":1},"s":[161.6395,354.6187],"t":0},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[161.6395,372.8421],"t":18},{"o":{"x":0.65,"y":0},"i":{"x":1,"y":1},"s":[161.6395,354.6187],"t":36},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[161.6395,372.8421],"t":54},{"o":{"x":0.65,"y":0},"i":{"x":1,"y":1},"s":[161.6395,354.6187],"t":72},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[161.6395,372.8421],"t":90},{"o":{"x":0.65,"y":0},"i":{"x":1,"y":1},"s":[161.6395,354.6187],"t":108},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[161.6395,372.8421],"t":126},{"s":[161.6395,354.6187],"t":144}]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"shapes":[{"ty":"gr","bm":0,"hd":false,"nm":"Group 2","it":[{"ty":"sh","bm":0,"hd":false,"nm":"Path 2","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[-0.30000000000000426,-0.09999999999999432],[0,0],[-7.5,3.6999999999999886],[0,0],[0,-0.30000000000001137],[0,0],[0.20000000000000284,0],[0,0],[7.600000000000001,3.6000000000000227],[0,0],[0,0.19999999999998863],[0,0]],"o":[[0,-0.4000000000000057],[0,0],[7.600000000000001,3.5999999999999943],[0,0],[0.29999999999999716,-0.19999999999998863],[0,0],[0,0.19999999999998863],[0,0],[-7.5,3.6999999999999886],[0,0],[-0.20000000000000284,-0.09999999999999432],[0,0],[0,0]],"v":[[43.5,159.3],[44.2,158.8],[45.800000000000004,159.5],[69.7,159.3],[70.8,158.70000000000002],[71.5,159.10000000000002],[71.5,161.50000000000003],[71.2,161.90000000000003],[69.60000000000001,162.70000000000005],[45.70000000000001,162.90000000000003],[43.70000000000001,162.00000000000003],[43.40000000000001,161.50000000000003],[43.40000000000001,159.3]]}}},{"ty":"fl","bm":0,"hd":false,"nm":"Fill","c":{"a":0,"k":[0,0,0]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","a":{"a":0,"k":[57.450000762939446,162.08674621582028]},"s":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[87,47],"t":42},{"s":[100,100],"t":44}]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[57.450000762939446,162.08674621582028]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}}]},{"ty":"gr","bm":0,"hd":false,"nm":"Group 3","it":[{"ty":"sh","bm":0,"hd":false,"nm":"Path 3","d":1,"ks":{"a":0,"k":{"c":true,"i":[[4.2,0],[0,0],[-0.19999999999999996,0],[-3.6999999999999993,1.8000000000000114],[0,0],[0,-0.30000000000001137],[0,0],[0.20000000000000107,0],[0,0]],"o":[[0,0],[0.19999999999999996,0],[4,-0.09999999999999432],[0,0],[0.3000000000000007,-0.19999999999998863],[0,0],[0,0.19999999999998863],[0,0],[-3.799999999999997,1.9999999999999716]],"v":[[1,141.89999999999998],[1,138.39999999999998],[1.499999999999993,138.39999999999998],[13.200000000000003,135.6],[14.299999999999997,135],[14.999999999999993,135.4],[14.999999999999993,137.79999999999998],[14.699999999999989,138.20000000000002],[13.099999999999994,139.00000000000003]]}}},{"ty":"fl","bm":0,"hd":false,"nm":"Fill","c":{"a":0,"k":[0,0,0]},"r":2,"o":{"a":0,"k":100}},{"ty":"tr","a":{"a":0,"k":[8,138.41799926757812]},"s":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[87,47],"t":42},{"s":[100,100],"t":44}]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[8,138.41799926757812]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}}]}],"ind":1},{"ty":4,"nm":"Layer_2","sr":1,"st":0,"op":150,"ip":0,"hd":false,"ln":"Layer_2","ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[107,124.57199859619138]},"s":{"a":0,"k":[202.55267952915443,202.55267952915443]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0.55,"y":0.06},"i":{"x":1,"y":1},"s":[304.9456,302.634],"t":0},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[304.9456,320.8574],"t":18},{"o":{"x":0.55,"y":0.06},"i":{"x":1,"y":1},"s":[304.9456,302.634],"t":36},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[304.9456,320.8574],"t":54},{"o":{"x":0.55,"y":0.06},"i":{"x":1,"y":1},"s":[304.9456,302.634],"t":72},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[304.9456,320.8574],"t":90},{"o":{"x":0.55,"y":0.06},"i":{"x":1,"y":1},"s":[304.9456,302.634],"t":108},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[304.9456,320.8574],"t":126},{"s":[304.9456,302.634],"t":144}]},"r":{"a":1,"k":[{"o":{"x":0.55,"y":0.06},"i":{"x":1,"y":1},"s":[0],"t":0},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[30],"t":14},{"o":{"x":0.55,"y":0.06},"i":{"x":1,"y":1},"s":[0],"t":32},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[30],"t":50},{"o":{"x":0.55,"y":0.06},"i":{"x":1,"y":1},"s":[0],"t":68},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[30],"t":86},{"o":{"x":0.55,"y":0.06},"i":{"x":1,"y":1},"s":[0],"t":104},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[30],"t":122},{"s":[0],"t":140}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"shapes":[{"ty":"gr","bm":0,"hd":false,"nm":"Group 1","it":[{"ty":"sh","bm":0,"hd":false,"nm":"Path 1","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,-3.1000000000000014],[0,0],[0.6999999999999886,-0.5999999999999943],[0,0],[0,3.0999999999999943],[0,0],[-0.6999999999999886,0.5999999999999943]],"o":[[0,0],[2.1999999999999886,-2.1000000000000014],[0,0],[0,1],[0,0],[-2.200000000000003,2.0999999999999943],[0,0],[0,-1],[0,0]],"v":[[109.6,75.9],[152.1,35.2],[158,37.7],[158,78.9],[156.9,81.4],[114.4,122.10000000000001],[108.5,119.60000000000001],[108.5,78.4],[109.6,75.9]]}}},{"ty":"st","bm":0,"hd":false,"nm":"Stroke","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":0,"k":3},"c":{"a":0,"k":[0,0,0]}},{"ty":"fl","bm":0,"hd":false,"nm":"Fill","c":{"a":0,"k":[0.7804,0.8,0.8]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","a":{"a":0,"k":[106.99999999999997,124.5719985961914]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[106.99999999999997,124.5719985961914]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}}]}],"ind":2},{"ty":4,"nm":"Layer_5","sr":1,"st":0,"op":42,"ip":0,"hd":false,"ln":"Layer_5","ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[202.55267952915443,202.55267952915443]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0.55,"y":0.06},"i":{"x":1,"y":1},"s":[88.2142,50.3101],"t":0},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[88.2142,68.5335],"t":18},{"o":{"x":0.65,"y":0},"i":{"x":1,"y":1},"s":[88.2142,50.3101],"t":36},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[88.2142,68.5335],"t":54},{"o":{"x":0.65,"y":0},"i":{"x":1,"y":1},"s":[88.2142,50.3101],"t":72},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[88.2142,68.5335],"t":90},{"o":{"x":0.65,"y":0},"i":{"x":1,"y":1},"s":[88.2142,50.3101],"t":108},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[88.2142,68.5335],"t":126},{"s":[88.2142,50.3101],"t":144}]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"shapes":[{"ty":"gr","bm":0,"hd":false,"nm":"Shape Layer 1","it":[{"ty":"gr","bm":0,"hd":false,"nm":"Group 4","it":[{"ty":"sh","bm":0,"hd":false,"nm":"Path 4","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,-0.8999999999999773],[0.5999999999999996,-0.5999999999999943],[0,0]],"o":[[0,1],[0,0],[0.5999999999999996,0.8000000000000114]],"v":[[4.999999999999993,140.19999999999996],[3.999999999999993,142.69999999999996],[3.999999999999993,137.59999999999997]]}}},{"ty":"st","bm":0,"hd":false,"nm":"Stroke","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":0,"k":3},"c":{"a":0,"k":[0,0,0]}},{"ty":"fl","bm":0,"hd":false,"nm":"Fill","c":{"a":0,"k":[0.0235,0.0235,0.0235]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","a":{"a":0,"k":[4.5,140.15000152587885]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[4.5,140.15000152587885]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}}]},{"ty":"gr","bm":0,"hd":false,"nm":"Group 5","it":[{"ty":"el","bm":0,"hd":false,"nm":"Ellipse 1","d":1,"p":{"a":0,"k":[57.8,164.2]},"s":{"a":0,"k":[7.6,7.6]}},{"ty":"st","bm":0,"hd":false,"nm":"Stroke","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":0,"k":3},"c":{"a":0,"k":[0,0,0]}},{"ty":"fl","bm":0,"hd":false,"nm":"Fill","c":{"a":0,"k":[0.0235,0.0235,0.0235]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}}]},{"ty":"tr","a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}}]}],"ind":3},{"ty":4,"nm":"Layer_5","sr":1,"st":52,"op":144,"ip":52,"hd":false,"ln":"Layer_5","ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[202.55267952915443,202.55267952915443]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0.55,"y":0.06},"i":{"x":1,"y":1},"s":[88.2142,50.3101],"t":1},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[88.2142,68.5335],"t":19},{"o":{"x":0.65,"y":0},"i":{"x":1,"y":1},"s":[88.2142,50.3101],"t":37},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[88.2142,68.5335],"t":55},{"o":{"x":0.65,"y":0},"i":{"x":1,"y":1},"s":[88.2142,50.3101],"t":73},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[88.2142,68.5335],"t":91},{"o":{"x":0.65,"y":0},"i":{"x":1,"y":1},"s":[88.2142,50.3101],"t":109},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[88.2142,68.5335],"t":127},{"s":[88.2142,50.3101],"t":145}]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"shapes":[{"ty":"gr","bm":0,"hd":false,"nm":"Shape Layer 1","it":[{"ty":"gr","bm":0,"hd":false,"nm":"Group 4","it":[{"ty":"sh","bm":0,"hd":false,"nm":"Path 4","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0.5999999999999996,-0.5999999999999943],[0,0],[0,-0.8999999999999773]],"o":[[0,1],[0,0],[0.5999999999999996,0.8000000000000114],[0,0]],"v":[[5,140.2],[4,142.7],[4,137.6],[5,140.2]]}}},{"ty":"st","bm":0,"hd":false,"nm":"Stroke","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":0,"k":3},"c":{"a":0,"k":[0,0,0]}},{"ty":"fl","bm":0,"hd":false,"nm":"Fill","c":{"a":0,"k":[0.0235,0.0235,0.0235]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}}]},{"ty":"gr","bm":0,"hd":false,"nm":"Group 5","it":[{"ty":"el","bm":0,"hd":false,"nm":"Ellipse 1","d":1,"p":{"a":0,"k":[57.8,164.2]},"s":{"a":0,"k":[7.6,7.6]}},{"ty":"st","bm":0,"hd":false,"nm":"Stroke","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":0,"k":3},"c":{"a":0,"k":[0,0,0]}},{"ty":"fl","bm":0,"hd":false,"nm":"Fill","c":{"a":0,"k":[0.0235,0.0235,0.0235]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}}]},{"ty":"tr","a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}}]}],"ind":4},{"ty":4,"nm":"Layer_6","sr":1,"st":0,"op":150,"ip":0,"hd":false,"ln":"Layer_6","ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[25.150000572204576,161.44999694824216]},"s":{"a":0,"k":[202.55267952915443,202.55267952915443]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0.55,"y":0.06},"i":{"x":1,"y":1},"s":[139.1562,377.3314],"t":0},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[139.1562,395.5548],"t":18},{"o":{"x":0.65,"y":0},"i":{"x":1,"y":1},"s":[139.1562,377.3314],"t":36},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[139.1562,395.5548],"t":54},{"o":{"x":0.65,"y":0},"i":{"x":1,"y":1},"s":[139.1562,377.3314],"t":72},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[139.1562,395.5548],"t":90},{"o":{"x":0.65,"y":0},"i":{"x":1,"y":1},"s":[139.1562,377.3314],"t":108},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[139.1562,395.5548],"t":126},{"s":[139.1562,377.3314],"t":144}]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"shapes":[{"ty":"gr","bm":0,"hd":false,"nm":"Group 6","it":[{"ty":"sh","bm":0,"hd":false,"nm":"Path 5","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0.09999999999999787,0.09999999999999432],[0,0],[0,-0.4000000000000057],[0,0],[0.40000000000000213,0.19999999999998863],[0,0],[0,-0.4000000000000057],[0,0],[-0.09999999999999964,-0.09999999999999432],[0,0],[0,-0.09999999999999432],[0,0],[-0.09999999999999787,-0.09999999999999432],[0,0],[0,0.4000000000000057],[0,0],[-0.40000000000000213,-0.19999999999998863],[0,0],[0,0.4000000000000057],[0,0],[0.10000000000000142,0.09999999999999432],[0,0],[0,0.09999999999999432],[0,0]],"o":[[0,-0.19999999999998863],[0,0],[-0.3000000000000007,-0.19999999999998863],[0,0],[0,0.4000000000000057],[0,0],[-0.3000000000000007,-0.19999999999998863],[0,0],[0,0.19999999999998863],[0,0],[0.1999999999999993,0.09999999999999432],[0,0],[0,0.19999999999998863],[0,0],[0.3000000000000007,0.19999999999998863],[0,0],[0,-0.4000000000000057],[0,0],[0.29999999999999716,0.19999999999998863],[0,0],[0,-0.19999999999998863],[0,0],[-0.1999999999999993,-0.09999999999999432],[0,0],[0,0]],"v":[[27,152.8],[26.8,152.4],[24.3,150.6],[23.5,151],[23.5,157.6],[22.7,158],[15.7,153.9],[14.899999999999999,154.3],[14.899999999999999,156.60000000000002],[15.099999999999998,157.00000000000003],[23.099999999999998,161.70000000000002],[23.299999999999997,162.10000000000002],[23.299999999999997,170.10000000000002],[23.499999999999996,170.50000000000003],[25.999999999999996,172.30000000000004],[26.799999999999997,171.90000000000003],[26.799999999999997,164.80000000000004],[27.599999999999998,164.40000000000003],[34.599999999999994,168.50000000000003],[35.39999999999999,168.10000000000002],[35.39999999999999,165.8],[35.19999999999999,165.4],[27.19999999999999,160.70000000000002],[26.99999999999999,160.3],[26.99999999999999,152.8]]}}},{"ty":"fl","bm":0,"hd":false,"nm":"Fill","c":{"a":0,"k":[0.0235,0.0196,0.0196]},"r":2,"o":{"a":0,"k":100}},{"ty":"tr","a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}}]}],"ind":5},{"ty":4,"nm":"Layer_1","sr":1,"st":0,"op":150,"ip":0,"hd":false,"ln":"Layer_1","ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[65.15000152587889,139.39999389648435]},"s":{"a":0,"k":[202.55267952915443,202.5527]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0.55,"y":0.06},"i":{"x":1,"y":1},"s":[220.1773,332.6685],"t":0},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[220.1773,350.8919],"t":18},{"o":{"x":0.65,"y":0},"i":{"x":1,"y":1},"s":[220.1773,332.6685],"t":36},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[220.1773,350.8919],"t":54},{"o":{"x":0.65,"y":0},"i":{"x":1,"y":1},"s":[220.1773,332.6685],"t":72},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[220.1773,350.8919],"t":90},{"o":{"x":0.65,"y":0},"i":{"x":1,"y":1},"s":[220.1773,332.6685],"t":108},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[220.1773,350.8919],"t":126},{"s":[220.1773,332.6685],"t":144}]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"shapes":[{"ty":"gr","bm":0,"hd":false,"nm":"Group 7","it":[{"ty":"sh","bm":0,"hd":false,"nm":"Path 6","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,-6.900000000000006],[0,0],[4,-2.1999999999999886],[0,0],[15,8.199999999999989],[0,0],[0,4.599999999999994],[0,0],[-6.1,3.0999999999999943],[0,0],[-14.399999999999991,-7.400000000000006]],"o":[[0,0],[6.200000000000003,3.200000000000003],[0,0],[0,4.599999999999994],[0,0],[-14.900000000000006,8.199999999999989],[0,0],[-4,-2.1999999999999886],[0,0],[0,-6.900000000000006],[0,0],[14.399999999999999,-7.400000000000006],[0,0]],"v":[[88.1,92],[118.8,107.9],[128.8,124.30000000000001],[128.8,157.10000000000002],[122.30000000000001,168.10000000000002],[89.10000000000001,186.20000000000002],[41.10000000000001,186.20000000000002],[8,168],[1.5,157],[1.5,124.2],[11.5,107.80000000000001],[42.2,92],[88.1,92]]}}},{"ty":"st","bm":0,"hd":false,"nm":"Stroke","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":0,"k":3},"c":{"a":0,"k":[0,0,0]}},{"ty":"fl","bm":0,"hd":false,"nm":"Fill","c":{"a":0,"k":[0.7804,0.8,0.8]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}}]}],"ind":6},{"ty":4,"nm":"Layer_3","sr":1,"st":0,"op":150,"ip":0,"hd":false,"ln":"Layer_3","ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[61.081897735595696,101.68240356445312]},"s":{"a":0,"k":[202.55267952915443,202.55267952915443]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0.55,"y":0.06},"i":{"x":1,"y":1},"s":[211.9372,256.2705],"t":0},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[211.9372,274.4939],"t":18},{"o":{"x":0.65,"y":0},"i":{"x":1,"y":1},"s":[211.9372,256.2705],"t":36},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[211.9372,274.4939],"t":54},{"o":{"x":0.65,"y":0},"i":{"x":1,"y":1},"s":[211.9372,256.2705],"t":72},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[211.9372,274.4939],"t":90},{"o":{"x":0.65,"y":0},"i":{"x":1,"y":1},"s":[211.9372,256.2705],"t":108},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[211.9372,274.4939],"t":126},{"s":[211.9372,256.2705],"t":144}]},"r":{"a":1,"k":[{"o":{"x":0.55,"y":0.06},"i":{"x":1,"y":1},"s":[0],"t":0},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[30],"t":16},{"o":{"x":0.55,"y":0.06},"i":{"x":1,"y":1},"s":[0],"t":34},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[30],"t":52},{"o":{"x":0.55,"y":0.06},"i":{"x":1,"y":1},"s":[0],"t":70},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[30],"t":88},{"o":{"x":0.55,"y":0.06},"i":{"x":1,"y":1},"s":[0],"t":106},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[30],"t":124},{"s":[0],"t":142}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"shapes":[{"ty":"gr","bm":0,"hd":false,"nm":"Group 8","it":[{"ty":"sh","bm":0,"hd":false,"nm":"Path 7","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[-0.29999999999999716,-3.0999999999999996],[0,0],[0.5999999999999943,-0.7000000000000028],[0,0],[0.29999999999999716,3.0999999999999943],[0,0],[-0.6000000000000014,0.8000000000000043]],"o":[[0,0],[2,-2.3],[0,0],[0.09999999999999432,1],[0,0],[-2,2.299999999999997],[0,0],[-0.10000000000000853,-0.8999999999999986],[0,0]],"v":[[63.4,53.4],[102.1,9.100000000000001],[108.19999999999999,11.100000000000001],[111.89999999999999,52.1],[111.1,54.7],[72.4,99],[66.30000000000001,97],[62.60000000000001,56],[63.4,53.4]]}}},{"ty":"st","bm":0,"hd":false,"nm":"Stroke","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":0,"k":3},"c":{"a":0,"k":[0,0,0]}},{"ty":"fl","bm":0,"hd":false,"nm":"Fill","c":{"a":0,"k":[0.7804,0.8,0.8]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}}]}],"ind":7}],"v":"5.7.0","fr":30,"op":144,"ip":0,"assets":[]}
--------------------------------------------------------------------------------