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