From 0e393c52961d6540254a56fe130a3e7af795fc54 Mon Sep 17 00:00:00 2001
From: Dinar Khakimov <85668474+mdrlzy@users.noreply.github.com>
Date: Thu, 29 Jun 2023 02:39:43 +0600
Subject: [PATCH] #16: Currency pair alerts (#21)
Animated bottom navigation
fix key 0 was used
nav replaced with compose-destinations; bump kotlin, compose version
minor fix
Fix change currency for existing conditions, fix ratio input
Check pair alerts every 8 hours
refactoring condition item
minor fix
---
app/build.gradle | 28 ++-
app/src/main/AndroidManifest.xml | 2 +
.../taran/arkrate/data/assets/AssetsRepo.kt | 8 +-
.../space/taran/arkrate/data/db/AssetsDao.kt | 8 +
.../space/taran/arkrate/data/db/Database.kt | 11 +-
.../arkrate/data/db/PairAlertConditionRepo.kt | 53 +++++
.../data/worker/CurrencyMonitorWorker.kt | 55 +++++
.../space/taran/arkrate/di/AppComponent.kt | 6 +
.../space/taran/arkrate/di/NavDepContainer.kt | 17 ++
.../space/taran/arkrate/di/module/DBModule.kt | 3 +
.../java/space/taran/arkrate/domain/Alias.kt | 3 +
.../arkrate/domain/PairAlertCondition.kt | 29 +++
.../space/taran/arkrate/presentation/App.kt | 37 ++-
.../arkrate/presentation/MainActivity.kt | 16 +-
.../taran/arkrate/presentation/MainScreen.kt | 91 +++++---
.../addcurrency/AddCurrencyScreen.kt | 24 +-
.../presentation/assets/AssetsScreen.kt | 27 ++-
.../pairalert/PairAlertConditionScreen.kt | 212 ++++++++++++++++++
.../presentation/shared/SharedViewModel.kt | 128 +++++++++++
.../presentation/summary/SummaryScreen.kt | 2 +
.../presentation/summary/SummaryViewModel.kt | 6 +
.../presentation/ui/BottomNavigation.kt | 101 +++++++++
.../arkrate/presentation/ui/RateScaffold.kt | 29 +++
.../presentation/utils/NotificationUtils.kt | 72 ++++++
.../presentation/utils/ViewModelUtils.kt | 65 ++++++
.../java/space/taran/arkrate/utils/Config.kt | 15 --
.../space/taran/arkrate/utils/FormatUtils.kt | 9 +
app/src/main/res/drawable/ic_add.xml | 5 +
app/src/main/res/drawable/ic_list.xml | 5 +
app/src/main/res/drawable/ic_list_alt.xml | 5 +
.../main/res/drawable/ic_notifications.xml | 5 +
build.gradle | 4 +-
32 files changed, 985 insertions(+), 96 deletions(-)
create mode 100644 app/src/main/java/space/taran/arkrate/data/db/PairAlertConditionRepo.kt
create mode 100644 app/src/main/java/space/taran/arkrate/data/worker/CurrencyMonitorWorker.kt
create mode 100644 app/src/main/java/space/taran/arkrate/di/NavDepContainer.kt
create mode 100644 app/src/main/java/space/taran/arkrate/domain/Alias.kt
create mode 100644 app/src/main/java/space/taran/arkrate/domain/PairAlertCondition.kt
create mode 100644 app/src/main/java/space/taran/arkrate/presentation/pairalert/PairAlertConditionScreen.kt
create mode 100644 app/src/main/java/space/taran/arkrate/presentation/shared/SharedViewModel.kt
create mode 100644 app/src/main/java/space/taran/arkrate/presentation/ui/BottomNavigation.kt
create mode 100644 app/src/main/java/space/taran/arkrate/presentation/ui/RateScaffold.kt
create mode 100644 app/src/main/java/space/taran/arkrate/presentation/utils/NotificationUtils.kt
create mode 100644 app/src/main/java/space/taran/arkrate/presentation/utils/ViewModelUtils.kt
delete mode 100644 app/src/main/java/space/taran/arkrate/utils/Config.kt
create mode 100644 app/src/main/res/drawable/ic_add.xml
create mode 100644 app/src/main/res/drawable/ic_list.xml
create mode 100644 app/src/main/res/drawable/ic_list_alt.xml
create mode 100644 app/src/main/res/drawable/ic_notifications.xml
diff --git a/app/build.gradle b/app/build.gradle
index 9010d53c0..6d38a4fd0 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -2,6 +2,7 @@ plugins {
id 'com.android.application'
id 'kotlin-kapt'
id 'org.jetbrains.kotlin.android'
+ id 'com.google.devtools.ksp' version "1.8.21-1.0.11"
}
android {
@@ -77,20 +78,26 @@ android {
}
dependencies {
- implementation "androidx.compose.ui:ui:$compose_version"
+ implementation "androidx.compose.ui:ui:1.4.3"
implementation "androidx.navigation:navigation-compose:2.5.2"
- implementation "com.google.accompanist:accompanist-navigation-animation:0.27.0"
- implementation "androidx.compose.material:material:$compose_version"
- implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
+ implementation "androidx.compose.material:material:1.4.3"
+ implementation "androidx.compose.ui:ui-tooling-preview:1.4.3"
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'
implementation 'androidx.activity:activity-compose:1.5.1'
implementation "com.google.dagger:dagger:2.42"
kapt "com.google.dagger:dagger-compiler:2.42"
- implementation "androidx.room:room-runtime:2.4.3"
- implementation "androidx.room:room-ktx:2.4.3"
- kapt "androidx.room:room-compiler:2.4.3"
+ implementation "androidx.room:room-runtime:2.5.1"
+ implementation "androidx.room:room-ktx:2.5.1"
+ kapt "androidx.room:room-compiler:2.5.1"
+
+ implementation 'com.jakewharton.timber:timber:5.0.1'
+
+ implementation "androidx.work:work-runtime-ktx:2.8.1"
+
+ implementation 'io.github.raamcosta.compose-destinations:core:1.7.41-beta'
+ ksp 'io.github.raamcosta.compose-destinations:ksp:1.7.41-beta'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
@@ -99,11 +106,10 @@ dependencies {
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
- androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
- debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
- debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version"
+ androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.4.3"
+ debugImplementation "androidx.compose.ui:ui-tooling:1.4.3"
+ debugImplementation "androidx.compose.ui:ui-test-manifest:1.4.3"
implementation 'ch.acra:acra-http:5.9.6'
implementation 'ch.acra:acra-dialog:5.9.6'
- implementation 'com.github.SimpleMobileTools:Simple-Commons:7e0240b1e3'
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 99683d64c..055cfd994 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -6,6 +6,8 @@
+
+
()
- private val currencyAmountFlow =
- MutableStateFlow>(emptyList())
private val scope = CoroutineScope(Dispatchers.IO)
init {
scope.launch {
currencyAmountList = local.getAll()
- currencyAmountFlow.emit(currencyAmountList)
}
}
fun allCurrencyAmount(): List = currencyAmountList
- fun allCurrencyAmountFlow(): StateFlow> = currencyAmountFlow
+ fun allCurrencyAmountFlow(): Flow> = local.allFlow()
suspend fun setCurrencyAmount(code: String, amount: Double) =
withContext(Dispatchers.IO) {
@@ -46,7 +44,6 @@ class AssetsRepo @Inject constructor(
currencyAmountList =
currencyAmountList + CurrencyAmount(code, amount)
}
- currencyAmountFlow.emit(currencyAmountList.toList())
local.insert(CurrencyAmount(code, amount))
}
@@ -54,7 +51,6 @@ class AssetsRepo @Inject constructor(
currencyAmountList.find { it.code == code }?.let {
currencyAmountList = currencyAmountList - it
}
- currencyAmountFlow.emit(currencyAmountList)
local.delete(code)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/space/taran/arkrate/data/db/AssetsDao.kt b/app/src/main/java/space/taran/arkrate/data/db/AssetsDao.kt
index 78b0212d8..f66355af1 100644
--- a/app/src/main/java/space/taran/arkrate/data/db/AssetsDao.kt
+++ b/app/src/main/java/space/taran/arkrate/data/db/AssetsDao.kt
@@ -6,6 +6,8 @@ import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.PrimaryKey
import androidx.room.Query
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
import space.taran.arkrate.data.CurrencyAmount
import javax.inject.Inject
@@ -24,6 +26,9 @@ interface AssetsDao {
@Query("SELECT * FROM RoomCurrencyAmount")
suspend fun getAll(): List
+ @Query("SELECT * FROM RoomCurrencyAmount")
+ fun allFlow(): Flow>
+
@Query("DELETE FROM RoomCurrencyAmount where code = :code")
suspend fun delete(code: String)
}
@@ -34,6 +39,9 @@ class AssetsLocalDataSource @Inject constructor(val dao: AssetsDao) {
suspend fun getAll() = dao.getAll().map { it.toCurrencyAmount() }
+ fun allFlow() =
+ dao.allFlow().map { list -> list.map { it.toCurrencyAmount() } }
+
suspend fun delete(code: String) = dao.delete(code)
}
diff --git a/app/src/main/java/space/taran/arkrate/data/db/Database.kt b/app/src/main/java/space/taran/arkrate/data/db/Database.kt
index ce7bcd460..1a55bdb01 100644
--- a/app/src/main/java/space/taran/arkrate/data/db/Database.kt
+++ b/app/src/main/java/space/taran/arkrate/data/db/Database.kt
@@ -1,20 +1,25 @@
package space.taran.arkrate.data.db
+import androidx.room.AutoMigration
import androidx.room.RoomDatabase
+import androidx.room.migration.Migration
+import androidx.sqlite.db.SupportSQLiteDatabase
@androidx.room.Database(
entities = [
RoomCurrencyAmount::class,
RoomCurrencyRate::class,
- RoomFetchTimestamp::class
+ RoomFetchTimestamp::class,
+ RoomPairAlertCondition::class
],
- version = 1,
- exportSchema = false
+ version = 2,
+ exportSchema = true
)
abstract class Database : RoomDatabase() {
abstract fun assetsDao(): AssetsDao
abstract fun rateDao(): CurrencyRateDao
abstract fun fetchTimestampDao(): FetchTimestampDao
+ abstract fun pairAlertDao(): PairAlertConditionDao
companion object {
const val DB_NAME = "arkrate.db"
diff --git a/app/src/main/java/space/taran/arkrate/data/db/PairAlertConditionRepo.kt b/app/src/main/java/space/taran/arkrate/data/db/PairAlertConditionRepo.kt
new file mode 100644
index 000000000..4c772b926
--- /dev/null
+++ b/app/src/main/java/space/taran/arkrate/data/db/PairAlertConditionRepo.kt
@@ -0,0 +1,53 @@
+package space.taran.arkrate.data.db
+
+import androidx.room.Dao
+import androidx.room.Entity
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.PrimaryKey
+import androidx.room.Query
+import space.taran.arkrate.domain.PairAlertCondition
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Entity
+data class RoomPairAlertCondition(
+ @PrimaryKey(autoGenerate = true)
+ val id: Long,
+ val numeratorCode: String,
+ val denominatorCode: String,
+ val ratio: Float,
+ val moreNotLess: Boolean
+)
+
+@Dao
+interface PairAlertConditionDao {
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ suspend fun insert(pairAlert: RoomPairAlertCondition): Long
+
+ @Query("SELECT * FROM RoomPairAlertCondition")
+ suspend fun getAll(): List
+
+ @Query("DELETE FROM RoomPairAlertCondition where id = :id")
+ suspend fun delete(id: Long)
+}
+
+private fun PairAlertCondition.toRoom() = RoomPairAlertCondition(
+ id, numeratorCode, denominatorCode, ratio, moreNotLess
+)
+
+private fun RoomPairAlertCondition.toCondition() = PairAlertCondition(
+ id, numeratorCode, denominatorCode, ratio, moreNotLess
+)
+
+@Singleton
+class PairAlertConditionRepo @Inject constructor(
+ private val dao: PairAlertConditionDao
+) {
+ suspend fun insert(pairAlertCondition: PairAlertCondition) =
+ dao.insert(pairAlertCondition.toRoom())
+
+ suspend fun getAll() = dao.getAll().map { it.toCondition() }
+
+ suspend fun delete(id: Long) = dao.delete(id)
+}
\ No newline at end of file
diff --git a/app/src/main/java/space/taran/arkrate/data/worker/CurrencyMonitorWorker.kt b/app/src/main/java/space/taran/arkrate/data/worker/CurrencyMonitorWorker.kt
new file mode 100644
index 000000000..5194b1f04
--- /dev/null
+++ b/app/src/main/java/space/taran/arkrate/data/worker/CurrencyMonitorWorker.kt
@@ -0,0 +1,55 @@
+package space.taran.arkrate.data.worker
+
+import android.content.Context
+import androidx.work.CoroutineWorker
+import androidx.work.WorkerParameters
+import space.taran.arkrate.data.CurrencyRate
+import space.taran.arkrate.data.GeneralCurrencyRepo
+import space.taran.arkrate.data.db.PairAlertConditionRepo
+import space.taran.arkrate.di.DIManager
+import space.taran.arkrate.domain.PairAlertCondition
+import space.taran.arkrate.presentation.utils.NotificationUtils
+import javax.inject.Inject
+
+class CurrencyMonitorWorker(val context: Context, params: WorkerParameters) :
+ CoroutineWorker(context, params) {
+
+ @Inject
+ lateinit var currencyRepo: GeneralCurrencyRepo
+
+ @Inject
+ lateinit var pairAlertRepo: PairAlertConditionRepo
+
+ override suspend fun doWork(): Result {
+ DIManager.component.inject(this)
+ val rates = currencyRepo.getCurrencyRate()
+ pairAlertRepo.getAll().forEach { pairAlert ->
+ val curRatio = curRatio(pairAlert, rates)
+ if (pairAlert.isConditionMet(curRatio)) {
+ notifyPair(pairAlert, curRatio)
+ }
+ }
+
+ return Result.success()
+ }
+
+ private fun curRatio(
+ pairAlertCondition: PairAlertCondition,
+ rates: List
+ ): Float {
+ val numeratorRate =
+ rates.find { it.code == pairAlertCondition.numeratorCode }!!.rate
+ val denominatorRate =
+ rates.find { it.code == pairAlertCondition.denominatorCode }!!.rate
+ return (numeratorRate / denominatorRate).toFloat()
+ }
+
+
+ private fun notifyPair(pairAlertCondition: PairAlertCondition, curRatio: Float) {
+ NotificationUtils.showPairAlert(pairAlertCondition, curRatio, context)
+ }
+
+ companion object {
+ const val name = "CurrencyMonitorWorker"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/space/taran/arkrate/di/AppComponent.kt b/app/src/main/java/space/taran/arkrate/di/AppComponent.kt
index 327c29476..b2f50cb31 100644
--- a/app/src/main/java/space/taran/arkrate/di/AppComponent.kt
+++ b/app/src/main/java/space/taran/arkrate/di/AppComponent.kt
@@ -6,11 +6,14 @@ import dagger.BindsInstance
import dagger.Component
import space.taran.arkrate.data.GeneralCurrencyRepo
import space.taran.arkrate.data.assets.AssetsRepo
+import space.taran.arkrate.data.worker.CurrencyMonitorWorker
import space.taran.arkrate.di.module.ApiModule
import space.taran.arkrate.di.module.DBModule
import space.taran.arkrate.presentation.summary.SummaryViewModelFactory
import space.taran.arkrate.presentation.addcurrency.AddCurrencyViewModelFactory
import space.taran.arkrate.presentation.assets.AssetsViewModelFactory
+import space.taran.arkrate.presentation.shared.SharedViewModel
+import space.taran.arkrate.presentation.shared.SharedViewModelFactory
import javax.inject.Singleton
@Singleton
@@ -24,8 +27,11 @@ interface AppComponent {
fun summaryViewModelFactory(): SummaryViewModelFactory
fun assetsVMFactory(): AssetsViewModelFactory
fun addCurrencyVMFactory(): AddCurrencyViewModelFactory
+ fun sharedVMFactory(): SharedViewModelFactory
+
fun generalCurrencyRepo(): GeneralCurrencyRepo
fun assetsRepo(): AssetsRepo
+ fun inject(currencyMonitorWorker: CurrencyMonitorWorker)
@Component.Factory
interface Factory {
diff --git a/app/src/main/java/space/taran/arkrate/di/NavDepContainer.kt b/app/src/main/java/space/taran/arkrate/di/NavDepContainer.kt
new file mode 100644
index 000000000..14e0be8fc
--- /dev/null
+++ b/app/src/main/java/space/taran/arkrate/di/NavDepContainer.kt
@@ -0,0 +1,17 @@
+package space.taran.arkrate.di
+
+import androidx.lifecycle.SavedStateHandle
+import space.taran.arkrate.presentation.MainActivity
+import space.taran.arkrate.presentation.shared.SharedViewModel
+
+class NavDepContainer(
+ val activity: MainActivity
+) {
+ @Suppress("UNCHECKED_CAST")
+ fun createViewModel(modelClass: Class, handle: SavedStateHandle): T {
+ return when (modelClass) {
+ SharedViewModel::class.java -> DIManager.component.sharedVMFactory().create(modelClass)
+ else -> throw RuntimeException("Unknown view model $modelClass")
+ } as T
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/space/taran/arkrate/di/module/DBModule.kt b/app/src/main/java/space/taran/arkrate/di/module/DBModule.kt
index f23a1d8c9..7f2907bcc 100644
--- a/app/src/main/java/space/taran/arkrate/di/module/DBModule.kt
+++ b/app/src/main/java/space/taran/arkrate/di/module/DBModule.kt
@@ -25,6 +25,9 @@ class DBModule {
@Provides
fun rateDao(db: Database) = db.rateDao()
+ @Provides
+ fun pairAlertDao(db: Database) = db.pairAlertDao()
+
@Provides
fun fetchTimestampDao(db: Database) = db.fetchTimestampDao()
}
\ No newline at end of file
diff --git a/app/src/main/java/space/taran/arkrate/domain/Alias.kt b/app/src/main/java/space/taran/arkrate/domain/Alias.kt
new file mode 100644
index 000000000..a7f679b1e
--- /dev/null
+++ b/app/src/main/java/space/taran/arkrate/domain/Alias.kt
@@ -0,0 +1,3 @@
+package space.taran.arkrate.domain
+
+typealias CurrencyCode = String
\ No newline at end of file
diff --git a/app/src/main/java/space/taran/arkrate/domain/PairAlertCondition.kt b/app/src/main/java/space/taran/arkrate/domain/PairAlertCondition.kt
new file mode 100644
index 000000000..26243130c
--- /dev/null
+++ b/app/src/main/java/space/taran/arkrate/domain/PairAlertCondition.kt
@@ -0,0 +1,29 @@
+package space.taran.arkrate.domain
+
+data class PairAlertCondition(
+ val id: Long,
+ val numeratorCode: String,
+ val denominatorCode: String,
+ val ratio: Float,
+ var moreNotLess: Boolean
+) {
+ fun isConditionMet(currentRatio: Float) =
+ if (moreNotLess)
+ currentRatio >= ratio
+ else
+ currentRatio <= ratio
+
+ fun isCompleted() =
+ numeratorCode.isNotEmpty() &&
+ denominatorCode.isNotEmpty()
+
+ companion object {
+ fun defaultInstance() = PairAlertCondition(
+ id = 0,
+ numeratorCode = "",
+ denominatorCode = "",
+ ratio = 1f,
+ moreNotLess = true
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/space/taran/arkrate/presentation/App.kt b/app/src/main/java/space/taran/arkrate/presentation/App.kt
index f1b3e626d..60f40ad88 100644
--- a/app/src/main/java/space/taran/arkrate/presentation/App.kt
+++ b/app/src/main/java/space/taran/arkrate/presentation/App.kt
@@ -1,6 +1,11 @@
package space.taran.arkrate.presentation
import android.app.Application
+import androidx.work.Constraints
+import androidx.work.ExistingPeriodicWorkPolicy
+import androidx.work.NetworkType
+import androidx.work.PeriodicWorkRequest
+import androidx.work.WorkManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -11,21 +16,21 @@ import org.acra.ktx.initAcra
import org.acra.sender.HttpSender
import space.taran.arkrate.BuildConfig
import space.taran.arkrate.R
+import space.taran.arkrate.data.worker.CurrencyMonitorWorker
import space.taran.arkrate.di.DIManager
-import space.taran.arkrate.utils.Config
+import java.util.concurrent.TimeUnit
-class App: Application() {
+class App : Application() {
override fun onCreate() {
super.onCreate()
initAcra()
DIManager.init(this)
+
+ initWorker()
}
private fun initAcra() = CoroutineScope(Dispatchers.IO).launch {
- val enabled = Config.newInstance(context = baseContext).crashReport
- if (!enabled) return@launch
-
initAcra {
buildConfigClass = BuildConfig::class.java
reportFormat = StringFormat.JSON
@@ -42,4 +47,26 @@ class App: Application() {
}
}
}
+
+ private fun initWorker() {
+ val workManager = WorkManager.getInstance(this)
+
+ val constraints = Constraints.Builder()
+ .setRequiredNetworkType(NetworkType.CONNECTED)
+ .build()
+
+ val workRequest =
+ PeriodicWorkRequest.Builder(
+ CurrencyMonitorWorker::class.java,
+ 8L,
+ TimeUnit.HOURS
+ ).setConstraints(constraints)
+ .build()
+
+ workManager.enqueueUniquePeriodicWork(
+ CurrencyMonitorWorker.name,
+ ExistingPeriodicWorkPolicy.KEEP,
+ workRequest
+ )
+ }
}
diff --git a/app/src/main/java/space/taran/arkrate/presentation/MainActivity.kt b/app/src/main/java/space/taran/arkrate/presentation/MainActivity.kt
index 1935cb17d..8cf2e4d20 100644
--- a/app/src/main/java/space/taran/arkrate/presentation/MainActivity.kt
+++ b/app/src/main/java/space/taran/arkrate/presentation/MainActivity.kt
@@ -1,18 +1,28 @@
package space.taran.arkrate.presentation
-import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
-import androidx.annotation.RequiresApi
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.staticCompositionLocalOf
+import space.taran.arkrate.di.NavDepContainer
import space.taran.arkrate.presentation.theme.ARKRateTheme
+val LocalDependencyContainer = staticCompositionLocalOf {
+ error("No dependency container provided!")
+}
+
class MainActivity : ComponentActivity() {
+
+ private val dependencyContainer by lazy { NavDepContainer(this) }
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ARKRateTheme {
- MainScreen()
+ CompositionLocalProvider(LocalDependencyContainer provides dependencyContainer) {
+ MainScreen()
+ }
}
}
}
diff --git a/app/src/main/java/space/taran/arkrate/presentation/MainScreen.kt b/app/src/main/java/space/taran/arkrate/presentation/MainScreen.kt
index ef48cf3a3..c3e19ffb7 100644
--- a/app/src/main/java/space/taran/arkrate/presentation/MainScreen.kt
+++ b/app/src/main/java/space/taran/arkrate/presentation/MainScreen.kt
@@ -1,48 +1,65 @@
package space.taran.arkrate.presentation
-import androidx.compose.animation.ExperimentalAnimationApi
-import androidx.compose.animation.fadeIn
-import androidx.compose.animation.fadeOut
+import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
-import com.google.accompanist.navigation.animation.AnimatedNavHost
-import com.google.accompanist.navigation.animation.composable
-import com.google.accompanist.navigation.animation.rememberAnimatedNavController
-import space.taran.arkrate.presentation.addcurrency.AddCurrencyScreen
-import space.taran.arkrate.presentation.assets.AssetsScreen
-import space.taran.arkrate.presentation.summary.SummaryScreen
-
-@OptIn(ExperimentalAnimationApi::class)
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.ui.Modifier
+import androidx.navigation.compose.currentBackStackEntryAsState
+import com.ramcosta.composedestinations.DestinationsNavHost
+import com.ramcosta.composedestinations.navigation.navigate
+import com.ramcosta.composedestinations.rememberNavHostEngine
+import space.taran.arkrate.presentation.destinations.AddCurrencyScreenDestination
+import space.taran.arkrate.presentation.ui.AnimatedRateBottomNavigation
+import space.taran.arkrate.presentation.ui.RateScaffold
+
+
@Composable
fun MainScreen() {
- val navController = rememberAnimatedNavController()
- AnimatedNavHost(navController, startDestination = Screen.Assets.name) {
- composable(Screen.Assets.name) {
- AssetsScreen(navController)
+ val engine = rememberNavHostEngine()
+ val navController = engine.rememberNavController()
+
+ val bottomBarVisible = rememberSaveable { mutableStateOf(false) }
+
+ val navBackStackEntry by navController.currentBackStackEntryAsState()
+
+ when (navBackStackEntry?.destination?.route) {
+ AddCurrencyScreenDestination.route -> {
+ bottomBarVisible.value = false
}
- composable(
- Screen.AddCurrency.name,
- enterTransition = {
- fadeIn()
- },
- exitTransition = {
- fadeOut()
- }
- ) {
- AddCurrencyScreen(navController)
+ else -> {
+ bottomBarVisible.value = true
}
- composable(Screen.Summary.name,
- enterTransition = {
- fadeIn()
- },
- exitTransition = {
- fadeOut()
- }
- ) {
- SummaryScreen()
+ }
+
+ RateScaffold(
+ navController = navController,
+ bottomBar = { destination ->
+ AnimatedRateBottomNavigation(
+ currentDestination = destination,
+ onBottomBarItemClick = {
+ navController.navigate(it) {
+ launchSingleTop = true
+ }
+ },
+ bottomBarVisible = bottomBarVisible
+ )
}
+ ) {
+ DestinationsNavHost(
+ engine = engine,
+ navController = navController,
+ navGraph = NavGraphs.root,
+ modifier = Modifier.padding(it)
+ )
}
}
-enum class Screen {
- Assets, AddCurrency, Summary
-}
\ No newline at end of file
+enum class Screen(val route: String) {
+ Assets("assets"),
+ AddCurrency("addCurrency/{from}"),
+ Summary("summary"),
+ PairAlert("pairAlert")
+}
+
diff --git a/app/src/main/java/space/taran/arkrate/presentation/addcurrency/AddCurrencyScreen.kt b/app/src/main/java/space/taran/arkrate/presentation/addcurrency/AddCurrencyScreen.kt
index abbfbf5ca..5901fc746 100644
--- a/app/src/main/java/space/taran/arkrate/presentation/addcurrency/AddCurrencyScreen.kt
+++ b/app/src/main/java/space/taran/arkrate/presentation/addcurrency/AddCurrencyScreen.kt
@@ -14,10 +14,22 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
+import com.ramcosta.composedestinations.annotation.Destination
import space.taran.arkrate.di.DIManager
+import space.taran.arkrate.presentation.destinations.AssetsScreenDestination
+import space.taran.arkrate.presentation.destinations.PairAlertConditionScreenDestination
+import space.taran.arkrate.presentation.shared.SharedViewModel
+import space.taran.arkrate.presentation.utils.activityViewModel
+@Destination
@Composable
-fun AddCurrencyScreen(navController: NavController) {
+fun AddCurrencyScreen(
+ navController: NavController,
+ sharedViewModel: SharedViewModel = activityViewModel(),
+ fromScreen: String,
+ numeratorNotDenominator: Boolean? = null,
+ pairAlertConditionId: Long? = null
+) {
val viewModel: AddCurrencyViewModel =
viewModel(factory = DIManager.component.addCurrencyVMFactory())
var filter by remember { mutableStateOf("") }
@@ -44,7 +56,15 @@ fun AddCurrencyScreen(navController: NavController) {
code = currencyName.code,
currency = currencyName.name,
onAdd = {
- viewModel.addCurrency(currencyName.code)
+ when (fromScreen) {
+ AssetsScreenDestination.route -> viewModel.addCurrency(currencyName.code)
+ PairAlertConditionScreenDestination.route ->
+ sharedViewModel.onAlertConditionCodePicked(
+ currencyName.code,
+ numeratorNotDenominator!!,
+ pairAlertConditionId!!
+ )
+ }
navController.popBackStack()
}
)
diff --git a/app/src/main/java/space/taran/arkrate/presentation/assets/AssetsScreen.kt b/app/src/main/java/space/taran/arkrate/presentation/assets/AssetsScreen.kt
index 626008eb6..0445c0a29 100644
--- a/app/src/main/java/space/taran/arkrate/presentation/assets/AssetsScreen.kt
+++ b/app/src/main/java/space/taran/arkrate/presentation/assets/AssetsScreen.kt
@@ -16,7 +16,6 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material.icons.filled.Delete
-import androidx.compose.material.icons.filled.List
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -27,14 +26,20 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
-import androidx.navigation.NavController
+import com.ramcosta.composedestinations.annotation.Destination
+import com.ramcosta.composedestinations.annotation.RootNavGraph
+import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import space.taran.arkrate.data.CurrencyAmount
import space.taran.arkrate.di.DIManager
import space.taran.arkrate.presentation.Screen
+import space.taran.arkrate.presentation.destinations.AddCurrencyScreenDestination
+import space.taran.arkrate.presentation.destinations.AssetsScreenDestination
import space.taran.arkrate.utils.removeFractionalPartIfEmpty
+@RootNavGraph(start = true)
+@Destination
@Composable
-fun AssetsScreen(navController: NavController) {
+fun AssetsScreen(navigator: DestinationsNavigator) {
val viewModel: AssetsViewModel =
viewModel(factory = DIManager.component.assetsVMFactory())
@@ -55,18 +60,16 @@ fun AssetsScreen(navController: NavController) {
modifier = Modifier
.align(Alignment.BottomStart)
.padding(10.dp),
- onClick = { navController.navigate(Screen.AddCurrency.name) },
+ onClick = {
+ navigator.navigate(
+ AddCurrencyScreenDestination(
+ fromScreen = AssetsScreenDestination.route
+ )
+ )
+ },
) {
Icon(Icons.Filled.Add, contentDescription = "Add")
}
- FloatingActionButton(
- modifier = Modifier
- .align(Alignment.BottomEnd)
- .padding(10.dp),
- onClick = { navController.navigate(Screen.Summary.name) },
- ) {
- Icon(Icons.Filled.List, contentDescription = "Summary")
- }
}
}
diff --git a/app/src/main/java/space/taran/arkrate/presentation/pairalert/PairAlertConditionScreen.kt b/app/src/main/java/space/taran/arkrate/presentation/pairalert/PairAlertConditionScreen.kt
new file mode 100644
index 000000000..2c2bf3c89
--- /dev/null
+++ b/app/src/main/java/space/taran/arkrate/presentation/pairalert/PairAlertConditionScreen.kt
@@ -0,0 +1,212 @@
+package space.taran.arkrate.presentation.pairalert
+
+import android.widget.Toast
+import androidx.compose.foundation.background
+import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.layout.wrapContentWidth
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material.Button
+import androidx.compose.material.Card
+import androidx.compose.material.Icon
+import androidx.compose.material.IconButton
+import androidx.compose.material.OutlinedTextField
+import androidx.compose.material.Text
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.Delete
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.ramcosta.composedestinations.annotation.Destination
+import com.ramcosta.composedestinations.navigation.DestinationsNavigator
+import space.taran.arkrate.domain.PairAlertCondition
+import space.taran.arkrate.presentation.destinations.AddCurrencyScreenDestination
+import space.taran.arkrate.presentation.destinations.PairAlertConditionScreenDestination
+import space.taran.arkrate.presentation.shared.SharedViewModel
+import space.taran.arkrate.presentation.utils.activityViewModel
+import space.taran.arkrate.utils.removeFractionalPartIfEmpty
+
+@Destination
+@Composable
+fun PairAlertConditionScreen(
+ navigator: DestinationsNavigator,
+ viewModel: SharedViewModel = activityViewModel()
+) {
+ Box(modifier = Modifier.fillMaxSize()) {
+ LazyColumn(modifier = Modifier.fillMaxSize()) {
+ items(
+ viewModel.pairAlertConditions,
+ key = { it.id }
+ ) { condition ->
+ ConditionItem(navigator, condition, viewModel)
+ }
+ item {
+ ConditionItem(navigator, viewModel.newCondition, viewModel)
+ }
+ }
+ }
+}
+
+@Composable
+private fun ConditionItem(
+ navigator: DestinationsNavigator,
+ condition: PairAlertCondition,
+ viewModel: SharedViewModel
+) {
+ var ratioInput by remember {
+ mutableStateOf(
+ if (condition.ratio == 0.0f) ""
+ else condition.ratio.removeFractionalPartIfEmpty()
+ )
+ }
+
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(10.dp),
+ ) {
+ Row(
+ modifier = Modifier
+ .padding(6.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Fraction(navigator, condition)
+ Button(
+ modifier = Modifier
+ .padding(horizontal = 8.dp)
+ .width(40.dp)
+ .wrapContentHeight(),
+ onClick = { viewModel.onConditionMoreLessChanged(condition) }
+ ) {
+ Text(if (condition.moreNotLess) ">" else "<")
+ }
+ OutlinedTextField(
+ modifier = Modifier
+ .weight(1f)
+ .wrapContentHeight()
+ .padding(start = 2.dp, end = 2.dp),
+ value = ratioInput,
+ onValueChange = { newInput ->
+ ratioInput = viewModel.onRatioChanged(
+ condition,
+ ratioInput,
+ newInput
+ )
+ },
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
+ )
+ AddOrDeleteBtn(viewModel, condition)
+ }
+ }
+}
+
+@Composable
+private fun Fraction(
+ navigator: DestinationsNavigator,
+ condition: PairAlertCondition
+) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Button(
+ modifier = Modifier
+ .wrapContentSize()
+ .padding(start = 2.dp, end = 2.dp),
+ onClick = {
+ navigator.navigate(
+ AddCurrencyScreenDestination(
+ fromScreen = PairAlertConditionScreenDestination.route,
+ numeratorNotDenominator = true,
+ pairAlertConditionId = condition.id
+ )
+ )
+ }
+ ) {
+ Text(condition.numeratorCode)
+ }
+ Box(
+ modifier = Modifier
+ .height(2.dp)
+ .width(60.dp)
+ .background(Color.LightGray)
+ )
+ Button(
+ modifier = Modifier
+ .wrapContentSize()
+ .padding(start = 2.dp, end = 2.dp),
+ onClick = {
+ navigator.navigate(
+ AddCurrencyScreenDestination(
+ fromScreen = PairAlertConditionScreenDestination.route,
+ numeratorNotDenominator = false,
+ pairAlertConditionId = condition.id
+ )
+ )
+ }
+ ) {
+ Text(condition.denominatorCode)
+ }
+ }
+}
+
+@Composable
+private fun AddOrDeleteBtn(
+ viewModel: SharedViewModel,
+ condition: PairAlertCondition
+) {
+ val ctx = LocalContext.current
+ if (viewModel.newCondition == condition) {
+ IconButton(
+ modifier = Modifier.padding(8.dp),
+ onClick = {
+ if (!condition.isCompleted()) {
+ Toast.makeText(
+ ctx,
+ "Pair alert is not completed",
+ Toast.LENGTH_SHORT
+ ).show()
+ return@IconButton
+ }
+ viewModel.onNewConditionSave()
+ }
+ ) {
+ Icon(Icons.Filled.Add, "Add")
+ }
+ } else {
+ IconButton(
+ modifier = Modifier.padding(8.dp),
+ onClick = {
+ Toast.makeText(
+ ctx,
+ "Remove pair ${condition.numeratorCode}/${condition.denominatorCode}",
+ Toast.LENGTH_SHORT
+ ).show()
+ viewModel.onRemoveCondition(condition)
+ }
+ ) {
+ Icon(Icons.Filled.Delete, "Delete")
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/space/taran/arkrate/presentation/shared/SharedViewModel.kt b/app/src/main/java/space/taran/arkrate/presentation/shared/SharedViewModel.kt
new file mode 100644
index 000000000..ad375c85d
--- /dev/null
+++ b/app/src/main/java/space/taran/arkrate/presentation/shared/SharedViewModel.kt
@@ -0,0 +1,128 @@
+package space.taran.arkrate.presentation.shared
+
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.launch
+import space.taran.arkrate.data.db.PairAlertConditionRepo
+import space.taran.arkrate.domain.CurrencyCode
+import space.taran.arkrate.domain.PairAlertCondition
+import space.taran.arkrate.utils.replace
+import javax.inject.Inject
+import javax.inject.Singleton
+
+class SharedViewModel(
+ private val alertConditionRepo: PairAlertConditionRepo
+) : ViewModel() {
+
+ var pairAlertConditions = mutableStateListOf()
+ var newCondition by mutableStateOf(PairAlertCondition.defaultInstance())
+
+ init {
+ viewModelScope.launch {
+ pairAlertConditions.addAll(alertConditionRepo.getAll())
+ }
+ }
+
+ fun onAlertConditionCodePicked(
+ code: CurrencyCode,
+ numeratorNotDenominator: Boolean,
+ conditionId: Long
+ ) {
+ if (conditionId == 0L) {
+ newCondition = if (numeratorNotDenominator)
+ newCondition.copy(numeratorCode = code)
+ else
+ newCondition.copy(denominatorCode = code)
+
+ return
+ }
+
+ val oldCondition = pairAlertConditions.find { it.id == conditionId }!!
+ val conditionIndex = pairAlertConditions.indexOf(oldCondition)
+ val updatedCondition = if (numeratorNotDenominator)
+ oldCondition.copy(numeratorCode = code)
+ else
+ oldCondition.copy(denominatorCode = code)
+
+ pairAlertConditions[conditionIndex] = updatedCondition
+ viewModelScope.launch {
+ alertConditionRepo.insert(updatedCondition)
+ }
+ }
+
+ fun onConditionMoreLessChanged(condition: PairAlertCondition) {
+ if (newCondition == condition) {
+ newCondition = newCondition.copy(moreNotLess = !newCondition.moreNotLess)
+ return
+ }
+
+ val conditionIndex = pairAlertConditions.indexOf(condition)
+ val updatedCondition =
+ condition.copy(moreNotLess = !condition.moreNotLess)
+ pairAlertConditions[conditionIndex] = updatedCondition
+ viewModelScope.launch {
+ alertConditionRepo.insert(updatedCondition)
+ }
+ }
+
+ fun onRatioChanged(
+ condition: PairAlertCondition,
+ oldInput: String,
+ newInput: String
+ ): String {
+ val containsDigitsAndDot = Regex("[0-9]*\\.?[0-9]*")
+ if (!containsDigitsAndDot.matches(newInput))
+ return oldInput
+
+ val containsDigit = Regex(".*[0-9].*")
+ if (!containsDigit.matches(newInput)) {
+ return newInput
+ }
+
+ val ratio = newInput.toFloat()
+
+ viewModelScope.launch {
+ if (newCondition == condition) {
+ newCondition = newCondition.copy(ratio = ratio)
+ return@launch
+ }
+
+ val conditionIndex = pairAlertConditions.indexOf(condition)
+ val updatedCondition =
+ condition.copy(ratio = ratio)
+ pairAlertConditions[conditionIndex] = updatedCondition
+ alertConditionRepo.insert(updatedCondition)
+ }
+
+ val leadingZeros = "^0+(?=\\d)".toRegex()
+
+ return newInput.replace(leadingZeros, "")
+ }
+
+ fun onRemoveCondition(condition: PairAlertCondition) = viewModelScope.launch {
+ alertConditionRepo.delete(condition.id)
+ pairAlertConditions.remove(condition)
+ }
+
+ fun onNewConditionSave() {
+ viewModelScope.launch {
+ val id = alertConditionRepo.insert(newCondition)
+ pairAlertConditions.add(newCondition.copy(id = id))
+ newCondition = PairAlertCondition.defaultInstance()
+ }
+ }
+}
+
+@Singleton
+class SharedViewModelFactory @Inject constructor(
+ private val alertConditionRepo: PairAlertConditionRepo
+) : ViewModelProvider.Factory {
+ override fun create(modelClass: Class): T {
+ return SharedViewModel(alertConditionRepo) as T
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/space/taran/arkrate/presentation/summary/SummaryScreen.kt b/app/src/main/java/space/taran/arkrate/presentation/summary/SummaryScreen.kt
index efc0ee98f..c042b5a0c 100644
--- a/app/src/main/java/space/taran/arkrate/presentation/summary/SummaryScreen.kt
+++ b/app/src/main/java/space/taran/arkrate/presentation/summary/SummaryScreen.kt
@@ -13,6 +13,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
+import com.ramcosta.composedestinations.annotation.Destination
import space.taran.arkrate.di.DIManager
import java.math.RoundingMode
import java.text.DecimalFormat
@@ -21,6 +22,7 @@ private val format = DecimalFormat("0.######").apply {
roundingMode = RoundingMode.HALF_DOWN
}
+@Destination
@Composable
fun SummaryScreen() {
val viewModel: SummaryViewModel =
diff --git a/app/src/main/java/space/taran/arkrate/presentation/summary/SummaryViewModel.kt b/app/src/main/java/space/taran/arkrate/presentation/summary/SummaryViewModel.kt
index 4b2aa0211..bb6ac64cb 100644
--- a/app/src/main/java/space/taran/arkrate/presentation/summary/SummaryViewModel.kt
+++ b/app/src/main/java/space/taran/arkrate/presentation/summary/SummaryViewModel.kt
@@ -4,6 +4,8 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import space.taran.arkrate.data.GeneralCurrencyRepo
import space.taran.arkrate.data.assets.AssetsRepo
@@ -22,6 +24,10 @@ class SummaryViewModel(
calculateTotal()
calculateExchange()
}
+ assetsRepo.allCurrencyAmountFlow().onEach {
+ calculateTotal()
+ calculateExchange()
+ }.launchIn(viewModelScope)
}
private suspend fun calculateTotal() {
diff --git a/app/src/main/java/space/taran/arkrate/presentation/ui/BottomNavigation.kt b/app/src/main/java/space/taran/arkrate/presentation/ui/BottomNavigation.kt
new file mode 100644
index 000000000..a57b91ef8
--- /dev/null
+++ b/app/src/main/java/space/taran/arkrate/presentation/ui/BottomNavigation.kt
@@ -0,0 +1,101 @@
+package space.taran.arkrate.presentation.ui
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.slideInVertically
+import androidx.compose.animation.slideOutVertically
+import androidx.compose.material.BottomNavigation
+import androidx.compose.material.BottomNavigationItem
+import androidx.compose.material.Icon
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.sp
+import androidx.navigation.compose.currentBackStackEntryAsState
+import com.ramcosta.composedestinations.spec.Direction
+import com.ramcosta.composedestinations.spec.DirectionDestinationSpec
+import space.taran.arkrate.R
+import space.taran.arkrate.presentation.destinations.AssetsScreenDestination
+import space.taran.arkrate.presentation.destinations.Destination
+import space.taran.arkrate.presentation.destinations.PairAlertConditionScreenDestination
+import space.taran.arkrate.presentation.destinations.SummaryScreenDestination
+
+sealed class BottomNavItem(
+ var title: String,
+ var icon: Int,
+ var direction: DirectionDestinationSpec
+) {
+ object Assets : BottomNavItem(
+ "Assets",
+ R.drawable.ic_list,
+ AssetsScreenDestination
+ )
+
+ object Summary : BottomNavItem(
+ "Summary",
+ R.drawable.ic_list_alt,
+ SummaryScreenDestination
+ )
+
+ object PairAlert : BottomNavItem(
+ "Notifications",
+ R.drawable.ic_notifications,
+ PairAlertConditionScreenDestination
+ )
+}
+
+@Composable
+fun AnimatedRateBottomNavigation(
+ currentDestination: Destination,
+ onBottomBarItemClick: (Direction) -> Unit,
+ bottomBarVisible: State
+) {
+ AnimatedVisibility(
+ visible = bottomBarVisible.value,
+ enter = slideInVertically(initialOffsetY = { it }),
+ exit = slideOutVertically(targetOffsetY = { it }),
+ content = { RateBottomNavigation(currentDestination, onBottomBarItemClick) }
+ )
+}
+
+@Composable
+fun RateBottomNavigation(
+ currentDestination: Destination,
+ onBottomBarItemClick: (Direction) -> Unit
+) {
+ val items = listOf(
+ BottomNavItem.Assets,
+ BottomNavItem.Summary,
+ BottomNavItem.PairAlert,
+ )
+
+ BottomNavigation(
+ backgroundColor = colorResource(id = R.color.teal_200),
+ contentColor = Color.Black
+ ) {
+ items.forEach { item ->
+ BottomNavigationItem(
+ icon = {
+ Icon(
+ painterResource(id = item.icon),
+ contentDescription = item.title
+ )
+ },
+ label = {
+ Text(
+ text = item.title,
+ fontSize = 9.sp
+ )
+ },
+ selectedContentColor = Color.Black,
+ unselectedContentColor = Color.Black.copy(0.4f),
+ alwaysShowLabel = true,
+ selected = currentDestination == item.direction,
+ onClick = { onBottomBarItemClick(item.direction) }
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/space/taran/arkrate/presentation/ui/RateScaffold.kt b/app/src/main/java/space/taran/arkrate/presentation/ui/RateScaffold.kt
new file mode 100644
index 000000000..43139eccc
--- /dev/null
+++ b/app/src/main/java/space/taran/arkrate/presentation/ui/RateScaffold.kt
@@ -0,0 +1,29 @@
+package space.taran.arkrate.presentation.ui
+
+import android.annotation.SuppressLint
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.material.Scaffold
+import androidx.compose.runtime.Composable
+import androidx.navigation.NavBackStackEntry
+import androidx.navigation.NavHostController
+import com.ramcosta.composedestinations.spec.Route
+import space.taran.arkrate.presentation.NavGraphs
+import space.taran.arkrate.presentation.appCurrentDestinationAsState
+import space.taran.arkrate.presentation.destinations.Destination
+import space.taran.arkrate.presentation.startAppDestination
+
+@SuppressLint("RestrictedApi")
+@Composable
+fun RateScaffold(
+ navController: NavHostController,
+ bottomBar: @Composable (Destination) -> Unit,
+ content: @Composable (PaddingValues) -> Unit,
+) {
+ val destination = navController.appCurrentDestinationAsState().value
+ ?: NavGraphs.root.startAppDestination
+
+ Scaffold(
+ bottomBar = { bottomBar(destination) },
+ content = content
+ )
+}
diff --git a/app/src/main/java/space/taran/arkrate/presentation/utils/NotificationUtils.kt b/app/src/main/java/space/taran/arkrate/presentation/utils/NotificationUtils.kt
new file mode 100644
index 000000000..d3f790ebd
--- /dev/null
+++ b/app/src/main/java/space/taran/arkrate/presentation/utils/NotificationUtils.kt
@@ -0,0 +1,72 @@
+package space.taran.arkrate.presentation.utils
+
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import androidx.core.content.ContextCompat
+import space.taran.arkrate.R
+import space.taran.arkrate.domain.PairAlertCondition
+import space.taran.arkrate.presentation.MainActivity
+import kotlin.random.Random
+
+object NotificationUtils {
+ fun showPairAlert(
+ pairAlertCondition: PairAlertCondition,
+ curRatio: Float,
+ ctx: Context
+ ) {
+ val pair = pairAlertCondition
+
+ val title = "❗ ${pair.numeratorCode}/${pair.denominatorCode}" +
+ " ${if (pair.moreNotLess) ">" else "<"} ${pair.ratio}"
+ val builder = NotificationCompat.Builder(ctx, CHANNEL_ID)
+ .setSmallIcon(R.drawable.ic_notifications)
+ .setContentTitle(title)
+ .setContentText(
+ "Current price of ${pair.numeratorCode} is $curRatio ${pair.denominatorCode}"
+ )
+ .setContentIntent(appIntent(ctx))
+ .setPriority(NotificationCompat.PRIORITY_DEFAULT)
+
+ createNotificationChannel(ctx)
+
+ with(NotificationManagerCompat.from(ctx)) {
+ notify(pairAlertCondition.id.toInt(), builder.build())
+ }
+ }
+
+ private fun appIntent(ctx: Context): PendingIntent {
+ val intent = Intent(ctx, MainActivity::class.java).apply {
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
+ }
+
+ return PendingIntent.getActivity(
+ ctx,
+ 0,
+ intent,
+ PendingIntent.FLAG_IMMUTABLE
+ )
+ }
+
+ private fun createNotificationChannel(ctx: Context) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val name = ctx.getString(R.string.app_name)
+ val importance = NotificationManager.IMPORTANCE_DEFAULT
+ val channel = NotificationChannel(CHANNEL_ID, name, importance)
+
+ val manager = ContextCompat.getSystemService(
+ ctx,
+ android.app.NotificationManager::class.java
+ )
+ channel.enableVibration(true)
+ manager?.createNotificationChannel(channel)
+ }
+ }
+
+ private const val CHANNEL_ID = "arkRate"
+}
\ No newline at end of file
diff --git a/app/src/main/java/space/taran/arkrate/presentation/utils/ViewModelUtils.kt b/app/src/main/java/space/taran/arkrate/presentation/utils/ViewModelUtils.kt
new file mode 100644
index 000000000..6a6699f8a
--- /dev/null
+++ b/app/src/main/java/space/taran/arkrate/presentation/utils/ViewModelUtils.kt
@@ -0,0 +1,65 @@
+package space.taran.arkrate.presentation.utils
+
+import android.os.Bundle
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalSavedStateRegistryOwner
+import androidx.lifecycle.AbstractSavedStateViewModelFactory
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelStoreOwner
+import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
+import androidx.navigation.NavBackStackEntry
+import androidx.savedstate.SavedStateRegistryOwner
+import space.taran.arkrate.di.NavDepContainer
+import space.taran.arkrate.presentation.LocalDependencyContainer
+
+@Composable
+inline fun viewModel(
+ viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
+ "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
+ },
+ savedStateRegistryOwner: SavedStateRegistryOwner = LocalSavedStateRegistryOwner.current
+): VM {
+ return androidx.lifecycle.viewmodel.compose.viewModel(
+ viewModelStoreOwner = viewModelStoreOwner,
+ factory = ViewModelFactory(
+ owner = savedStateRegistryOwner,
+ defaultArgs = (savedStateRegistryOwner as? NavBackStackEntry)?.arguments,
+ dependencyContainer = LocalDependencyContainer.current,
+ )
+ )
+}
+
+@Composable
+inline fun activityViewModel(): VM {
+ val activity = LocalDependencyContainer.current.activity
+
+ return androidx.lifecycle.viewmodel.compose.viewModel(
+ VM::class.java,
+ activity,
+ null,
+ factory = ViewModelFactory(
+ owner = activity,
+ defaultArgs = null,
+ dependencyContainer = LocalDependencyContainer.current,
+ )
+ )
+}
+
+class ViewModelFactory(
+ owner: SavedStateRegistryOwner,
+ defaultArgs: Bundle?,
+ private val dependencyContainer: NavDepContainer
+) : AbstractSavedStateViewModelFactory(
+ owner,
+ defaultArgs
+) {
+
+ override fun create(
+ key: String,
+ modelClass: Class,
+ handle: SavedStateHandle
+ ): T {
+ return dependencyContainer.createViewModel(modelClass, handle)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/space/taran/arkrate/utils/Config.kt b/app/src/main/java/space/taran/arkrate/utils/Config.kt
deleted file mode 100644
index c57a4eee2..000000000
--- a/app/src/main/java/space/taran/arkrate/utils/Config.kt
+++ /dev/null
@@ -1,15 +0,0 @@
-package space.taran.arkrate.utils
-
-import android.content.Context
-import com.simplemobiletools.commons.helpers.BaseConfig
-
-class Config(context: Context) : BaseConfig(context) {
- companion object {
- fun newInstance(context: Context) = Config(context)
- }
-
- var crashReport:Boolean
- get() = prefs.getBoolean(CRASH_REPORT_ENABLE, true)
- set(isEnable) = prefs.edit().putBoolean(CRASH_REPORT_ENABLE, isEnable)
- .apply()
-}
\ No newline at end of file
diff --git a/app/src/main/java/space/taran/arkrate/utils/FormatUtils.kt b/app/src/main/java/space/taran/arkrate/utils/FormatUtils.kt
index a82da5391..b984cbc87 100644
--- a/app/src/main/java/space/taran/arkrate/utils/FormatUtils.kt
+++ b/app/src/main/java/space/taran/arkrate/utils/FormatUtils.kt
@@ -7,4 +7,13 @@ fun Double.removeFractionalPartIfEmpty(): String {
integerPart.toString()
else
this.toString()
+}
+
+fun Float.removeFractionalPartIfEmpty(): String {
+ val integerPart = this.toInt()
+ val fractionalPart = integerPart - this
+ return if (fractionalPart == 0.0f)
+ integerPart.toString()
+ else
+ this.toString()
}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_add.xml b/app/src/main/res/drawable/ic_add.xml
new file mode 100644
index 000000000..89633bb12
--- /dev/null
+++ b/app/src/main/res/drawable/ic_add.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_list.xml b/app/src/main/res/drawable/ic_list.xml
new file mode 100644
index 000000000..7483a9873
--- /dev/null
+++ b/app/src/main/res/drawable/ic_list.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_list_alt.xml b/app/src/main/res/drawable/ic_list_alt.xml
new file mode 100644
index 000000000..57fd02706
--- /dev/null
+++ b/app/src/main/res/drawable/ic_list_alt.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_notifications.xml b/app/src/main/res/drawable/ic_notifications.xml
new file mode 100644
index 000000000..1d038a440
--- /dev/null
+++ b/app/src/main/res/drawable/ic_notifications.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/build.gradle b/build.gradle
index 54694c7f1..a4f38b613 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,8 +1,8 @@
buildscript {
ext {
- compose_version = '1.3.0'
+ compose_version = '1.4.7'
}
- ext.kotlin_version="1.7.10"
+ ext.kotlin_version="1.8.21"
}// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id 'com.android.application' version '7.2.2' apply false