From f956f9598fb729809a9d28445009a5bb7dabef89 Mon Sep 17 00:00:00 2001 From: Dinar Khakimov <85668474+mdrlzy@users.noreply.github.com> Date: Sat, 12 Nov 2022 17:10:13 +0600 Subject: [PATCH] #8: Global refactoring (#13) * use StateList, models refactoring * Caching with Room, NetworkStatus implemented * Added user agent * fix Key [CurrencyCode] was already used java.lang.IllegalArgumentException: Key TRX was already used. If you are using LazyColumn/Row please make sure you provide a unique key for each item. * Format amount input * Fix api links --- app/build.gradle | 20 +- app/src/main/AndroidManifest.xml | 5 +- .../taran/arkrate/data/CurrencyAmount.kt | 6 + .../space/taran/arkrate/data/CurrencyName.kt | 6 + .../space/taran/arkrate/data/CurrencyRate.kt | 6 + .../space/taran/arkrate/data/CurrencyRepo.kt | 56 +++++ .../space/taran/arkrate/data/CurrencyType.kt | 5 + .../taran/arkrate/data/GeneralCurrencyRepo.kt | 27 +++ .../taran/arkrate/data/assets/AssetsRepo.kt | 60 +++++ .../taran/arkrate/data/crypto/CryptoAPI.kt | 8 + .../arkrate/data/crypto/CryptoCurrencyRepo.kt | 39 +++ .../data/crypto/CryptoRatesResponse.kt | 6 + .../space/taran/arkrate/data/db/AssetsDao.kt | 41 ++++ .../taran/arkrate/data/db/CurrencyRateDao.kt | 42 ++++ .../space/taran/arkrate/data/db/Database.kt | 22 ++ .../arkrate/data/db/FetchTimestampDao.kt | 35 +++ .../space/taran/arkrate/data/fiat/FiatAPI.kt | 8 + .../arkrate/data/fiat/FiatCurrencyRepo.kt | 204 ++++++++++++++++ .../arkrate/data/fiat/FiatRateResponse.kt | 6 + .../arkrate/data/network/NetworkStatus.kt | 30 +++ .../data/network/OkHttpClientBuilder.kt | 36 +++ .../space/taran/arkrate/di/AppComponent.kt | 37 +++ .../java/space/taran/arkrate/di/DIManager.kt | 12 + .../taran/arkrate/di/module/ApiModule.kt | 44 ++++ .../space/taran/arkrate/di/module/DBModule.kt | 30 +++ .../space/taran/arkrate/network/crypto.kt | 74 ------ .../space/taran/arkrate/network/currencies.kt | 192 --------------- .../java/space/taran/arkrate/network/rates.kt | 50 ---- .../taran/arkrate/{ => presentation}/App.kt | 6 +- .../{ => presentation}/MainActivity.kt | 11 +- .../taran/arkrate/presentation/MainScreen.kt | 48 ++++ .../addcurrency/AddCurrencyScreen.kt | 78 ++++++ .../addcurrency/AddCurrencyViewModel.kt | 41 ++++ .../presentation/assets/AssetsScreen.kt | 120 ++++++++++ .../presentation/assets/AssetsViewModel.kt | 61 +++++ .../presentation/summary/SummaryScreen.kt | 122 ++++++++++ .../presentation/summary/SummaryViewModel.kt | 73 ++++++ .../{ui => presentation}/theme/Color.kt | 2 +- .../{ui => presentation}/theme/Shape.kt | 2 +- .../{ui => presentation}/theme/Theme.kt | 4 +- .../{ui => presentation}/theme/Type.kt | 2 +- .../java/space/taran/arkrate/storage/Input.kt | 74 ------ .../space/taran/arkrate/ui/AddExchange.kt | 78 ------ .../space/taran/arkrate/ui/InputActivity.kt | 111 --------- .../space/taran/arkrate/ui/MainActivity.kt | 220 ----------------- .../space/taran/arkrate/ui/OutputActivity.kt | 224 ------------------ .../taran/arkrate/utils/CollectionsUtils.kt | 9 + .../taran/arkrate/utils/CoroutineUtils.kt | 17 ++ .../space/taran/arkrate/utils/FormatUtils.kt | 10 + app/src/main/res/drawable/loading.gif | Bin 61465 -> 0 bytes build.gradle | 2 +- 51 files changed, 1378 insertions(+), 1044 deletions(-) create mode 100644 app/src/main/java/space/taran/arkrate/data/CurrencyAmount.kt create mode 100644 app/src/main/java/space/taran/arkrate/data/CurrencyName.kt create mode 100644 app/src/main/java/space/taran/arkrate/data/CurrencyRate.kt create mode 100644 app/src/main/java/space/taran/arkrate/data/CurrencyRepo.kt create mode 100644 app/src/main/java/space/taran/arkrate/data/CurrencyType.kt create mode 100644 app/src/main/java/space/taran/arkrate/data/GeneralCurrencyRepo.kt create mode 100644 app/src/main/java/space/taran/arkrate/data/assets/AssetsRepo.kt create mode 100644 app/src/main/java/space/taran/arkrate/data/crypto/CryptoAPI.kt create mode 100644 app/src/main/java/space/taran/arkrate/data/crypto/CryptoCurrencyRepo.kt create mode 100644 app/src/main/java/space/taran/arkrate/data/crypto/CryptoRatesResponse.kt create mode 100644 app/src/main/java/space/taran/arkrate/data/db/AssetsDao.kt create mode 100644 app/src/main/java/space/taran/arkrate/data/db/CurrencyRateDao.kt create mode 100644 app/src/main/java/space/taran/arkrate/data/db/Database.kt create mode 100644 app/src/main/java/space/taran/arkrate/data/db/FetchTimestampDao.kt create mode 100644 app/src/main/java/space/taran/arkrate/data/fiat/FiatAPI.kt create mode 100644 app/src/main/java/space/taran/arkrate/data/fiat/FiatCurrencyRepo.kt create mode 100644 app/src/main/java/space/taran/arkrate/data/fiat/FiatRateResponse.kt create mode 100644 app/src/main/java/space/taran/arkrate/data/network/NetworkStatus.kt create mode 100644 app/src/main/java/space/taran/arkrate/data/network/OkHttpClientBuilder.kt create mode 100644 app/src/main/java/space/taran/arkrate/di/AppComponent.kt create mode 100644 app/src/main/java/space/taran/arkrate/di/DIManager.kt create mode 100644 app/src/main/java/space/taran/arkrate/di/module/ApiModule.kt create mode 100644 app/src/main/java/space/taran/arkrate/di/module/DBModule.kt delete mode 100644 app/src/main/java/space/taran/arkrate/network/crypto.kt delete mode 100644 app/src/main/java/space/taran/arkrate/network/currencies.kt delete mode 100644 app/src/main/java/space/taran/arkrate/network/rates.kt rename app/src/main/java/space/taran/arkrate/{ => presentation}/App.kt (87%) rename app/src/main/java/space/taran/arkrate/{ => presentation}/MainActivity.kt (59%) create mode 100644 app/src/main/java/space/taran/arkrate/presentation/MainScreen.kt create mode 100644 app/src/main/java/space/taran/arkrate/presentation/addcurrency/AddCurrencyScreen.kt create mode 100644 app/src/main/java/space/taran/arkrate/presentation/addcurrency/AddCurrencyViewModel.kt create mode 100644 app/src/main/java/space/taran/arkrate/presentation/assets/AssetsScreen.kt create mode 100644 app/src/main/java/space/taran/arkrate/presentation/assets/AssetsViewModel.kt create mode 100644 app/src/main/java/space/taran/arkrate/presentation/summary/SummaryScreen.kt create mode 100644 app/src/main/java/space/taran/arkrate/presentation/summary/SummaryViewModel.kt rename app/src/main/java/space/taran/arkrate/{ui => presentation}/theme/Color.kt (79%) rename app/src/main/java/space/taran/arkrate/{ui => presentation}/theme/Shape.kt (85%) rename app/src/main/java/space/taran/arkrate/{ui => presentation}/theme/Theme.kt (87%) rename app/src/main/java/space/taran/arkrate/{ui => presentation}/theme/Type.kt (94%) delete mode 100644 app/src/main/java/space/taran/arkrate/storage/Input.kt delete mode 100644 app/src/main/java/space/taran/arkrate/ui/AddExchange.kt delete mode 100644 app/src/main/java/space/taran/arkrate/ui/InputActivity.kt delete mode 100644 app/src/main/java/space/taran/arkrate/ui/MainActivity.kt delete mode 100644 app/src/main/java/space/taran/arkrate/ui/OutputActivity.kt create mode 100644 app/src/main/java/space/taran/arkrate/utils/CollectionsUtils.kt create mode 100644 app/src/main/java/space/taran/arkrate/utils/CoroutineUtils.kt create mode 100644 app/src/main/java/space/taran/arkrate/utils/FormatUtils.kt delete mode 100644 app/src/main/res/drawable/loading.gif diff --git a/app/build.gradle b/app/build.gradle index f952e32b7..a07bd4547 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -75,17 +75,25 @@ android { } dependencies { - implementation 'com.squareup.moshi:moshi-kotlin:1.14.0' - kapt 'com.squareup.moshi:moshi-kotlin-codegen:1.14.0' - implementation "com.squareup.okhttp3:okhttp:5.0.0-alpha.3" implementation "androidx.compose.ui:ui:$compose_version" - implementation 'io.coil-kt:coil:2.2.1' - implementation 'io.coil-kt:coil-gif:2.2.1' - implementation 'io.coil-kt:coil-compose:2.2.1' + 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.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 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.retrofit2:converter-gson:2.9.0' + implementation 'com.squareup.okhttp3:logging-interceptor:4.9.1' + testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index cafbf3819..99683d64c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,10 +3,11 @@ xmlns:tools="http://schemas.android.com/tools" package="space.taran.arkrate"> + diff --git a/app/src/main/java/space/taran/arkrate/data/CurrencyAmount.kt b/app/src/main/java/space/taran/arkrate/data/CurrencyAmount.kt new file mode 100644 index 000000000..e3dbfcaf9 --- /dev/null +++ b/app/src/main/java/space/taran/arkrate/data/CurrencyAmount.kt @@ -0,0 +1,6 @@ +package space.taran.arkrate.data + +data class CurrencyAmount( + val code: String, + var amount: Double +) \ No newline at end of file diff --git a/app/src/main/java/space/taran/arkrate/data/CurrencyName.kt b/app/src/main/java/space/taran/arkrate/data/CurrencyName.kt new file mode 100644 index 000000000..0edf0d617 --- /dev/null +++ b/app/src/main/java/space/taran/arkrate/data/CurrencyName.kt @@ -0,0 +1,6 @@ +package space.taran.arkrate.data + +data class CurrencyName( + val code: String, + val name: String +) \ No newline at end of file diff --git a/app/src/main/java/space/taran/arkrate/data/CurrencyRate.kt b/app/src/main/java/space/taran/arkrate/data/CurrencyRate.kt new file mode 100644 index 000000000..f30043361 --- /dev/null +++ b/app/src/main/java/space/taran/arkrate/data/CurrencyRate.kt @@ -0,0 +1,6 @@ +package space.taran.arkrate.data + +data class CurrencyRate( + val code: String, + val rate: Double +) \ No newline at end of file diff --git a/app/src/main/java/space/taran/arkrate/data/CurrencyRepo.kt b/app/src/main/java/space/taran/arkrate/data/CurrencyRepo.kt new file mode 100644 index 000000000..d48665b48 --- /dev/null +++ b/app/src/main/java/space/taran/arkrate/data/CurrencyRepo.kt @@ -0,0 +1,56 @@ +package space.taran.arkrate.data + +import android.util.Log +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import space.taran.arkrate.data.db.CurrencyRateLocalDataSource +import space.taran.arkrate.data.db.FetchTimestampDataSource +import space.taran.arkrate.data.network.NetworkStatus +import space.taran.arkrate.utils.withContextAndLock +import java.util.concurrent.TimeUnit + +abstract class CurrencyRepo( + private val local: CurrencyRateLocalDataSource, + private val networkStatus: NetworkStatus, + private val fetchTimestampDataSource: FetchTimestampDataSource +) { + protected abstract val type: CurrencyType + private var currencyRates: List? = null + private var updatedTS: Long? = null + private val mutex = Mutex() + + suspend fun getCurrencyRate(): List = + withContextAndLock(Dispatchers.IO, mutex) { + if (!networkStatus.isOnline()) { + currencyRates = local.getByType(type) + return@withContextAndLock currencyRates!! + } + + updatedTS ?: let { + updatedTS = fetchTimestampDataSource.getTimestamp(type) + } + + if ( + updatedTS == null || + updatedTS!! + dayInMillis < System.currentTimeMillis() + ) { + currencyRates = fetchRemote() + launch { fetchTimestampDataSource.rememberTimestamp(type) } + launch { local.insert(currencyRates!!, type) } + updatedTS = System.currentTimeMillis() + } + + currencyRates ?: let { + currencyRates = local.getByType(type) + } + Log.d("Test", "${currencyRates!!.sortedBy { it.code }}") + return@withContextAndLock currencyRates!! + } + + protected abstract suspend fun fetchRemote(): List + + abstract suspend fun getCurrencyName(): List + + private val dayInMillis = TimeUnit.DAYS.toMillis(1) +} \ No newline at end of file diff --git a/app/src/main/java/space/taran/arkrate/data/CurrencyType.kt b/app/src/main/java/space/taran/arkrate/data/CurrencyType.kt new file mode 100644 index 000000000..094391899 --- /dev/null +++ b/app/src/main/java/space/taran/arkrate/data/CurrencyType.kt @@ -0,0 +1,5 @@ +package space.taran.arkrate.data + +enum class CurrencyType { + FIAT, CRYPTO +} \ No newline at end of file diff --git a/app/src/main/java/space/taran/arkrate/data/GeneralCurrencyRepo.kt b/app/src/main/java/space/taran/arkrate/data/GeneralCurrencyRepo.kt new file mode 100644 index 000000000..896247077 --- /dev/null +++ b/app/src/main/java/space/taran/arkrate/data/GeneralCurrencyRepo.kt @@ -0,0 +1,27 @@ +package space.taran.arkrate.data + +import space.taran.arkrate.data.crypto.CryptoCurrencyRepo +import space.taran.arkrate.data.fiat.FiatCurrencyRepo +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class GeneralCurrencyRepo @Inject constructor( + val fiatRepo: FiatCurrencyRepo, + val cryptoRepo: CryptoCurrencyRepo +) { + private val currencyRepos = listOf( + fiatRepo, + cryptoRepo + ) + + suspend fun getCurrencyRate(): List = + currencyRepos.fold(emptyList()) { codeToRate, repo -> + codeToRate + repo.getCurrencyRate() + } + + suspend fun getCurrencyName(): List = + currencyRepos.fold(emptyList()) { currencyName, repo -> + currencyName + repo.getCurrencyName() + } +} \ No newline at end of file diff --git a/app/src/main/java/space/taran/arkrate/data/assets/AssetsRepo.kt b/app/src/main/java/space/taran/arkrate/data/assets/AssetsRepo.kt new file mode 100644 index 000000000..80a00d9ae --- /dev/null +++ b/app/src/main/java/space/taran/arkrate/data/assets/AssetsRepo.kt @@ -0,0 +1,60 @@ +package space.taran.arkrate.data.assets + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import space.taran.arkrate.data.CurrencyAmount +import space.taran.arkrate.data.db.AssetsLocalDataSource +import space.taran.arkrate.utils.replace +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class AssetsRepo @Inject constructor( + private val local: AssetsLocalDataSource +) { + private var currencyAmountList = listOf() + 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 + + suspend fun setCurrencyAmount(code: String, amount: Double) = + withContext(Dispatchers.IO) { + currencyAmountList.find { + it.code == code + }?.let { currencyAmount -> + currencyAmountList = currencyAmountList.replace( + currencyAmount, + currencyAmount.copy(amount = amount) + ) + } ?: let { + currencyAmountList = + currencyAmountList + CurrencyAmount(code, amount) + } + currencyAmountFlow.emit(currencyAmountList.toList()) + local.insert(CurrencyAmount(code, amount)) + } + + suspend fun removeCurrency(code: String) = withContext(Dispatchers.IO) { + 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/crypto/CryptoAPI.kt b/app/src/main/java/space/taran/arkrate/data/crypto/CryptoAPI.kt new file mode 100644 index 000000000..a4c2ca733 --- /dev/null +++ b/app/src/main/java/space/taran/arkrate/data/crypto/CryptoAPI.kt @@ -0,0 +1,8 @@ +package space.taran.arkrate.data.crypto + +import retrofit2.http.GET + +interface CryptoAPI { + @GET("/ARK-Builders/ark-exchange-rates/main/crypto-rates.json") + suspend fun getCryptoRates(): List +} \ No newline at end of file diff --git a/app/src/main/java/space/taran/arkrate/data/crypto/CryptoCurrencyRepo.kt b/app/src/main/java/space/taran/arkrate/data/crypto/CryptoCurrencyRepo.kt new file mode 100644 index 000000000..91da7c321 --- /dev/null +++ b/app/src/main/java/space/taran/arkrate/data/crypto/CryptoCurrencyRepo.kt @@ -0,0 +1,39 @@ +package space.taran.arkrate.data.crypto + +import space.taran.arkrate.data.CurrencyName +import space.taran.arkrate.data.CurrencyRate +import space.taran.arkrate.data.CurrencyRepo +import space.taran.arkrate.data.CurrencyType +import space.taran.arkrate.data.network.NetworkStatus +import space.taran.arkrate.data.db.CurrencyRateLocalDataSource +import space.taran.arkrate.data.db.FetchTimestampDataSource +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class CryptoCurrencyRepo @Inject constructor( + private val cryptoAPI: CryptoAPI, + private val local: CurrencyRateLocalDataSource, + private val networkStatus: NetworkStatus, + private val fetchTimestampDataSource: FetchTimestampDataSource +) : CurrencyRepo(local, networkStatus, fetchTimestampDataSource) { + override val type = CurrencyType.CRYPTO + + override suspend fun fetchRemote(): List = + cryptoAPI.getCryptoRates().findUSDTPairs() + + override suspend fun getCurrencyName(): List = + getCurrencyRate().map { + CurrencyName(it.code, name = "") + } + + // api returns pairs like ETHBTC, ETHBNB, ETHTRX, ETHUSDT + // we only take USDT pairs + private fun List.findUSDTPairs() = + mapNotNull { (code, price) -> + if (code.takeLast(4) == "USDT") { + CurrencyRate(code.dropLast(4), price) + } else + null + } +} \ No newline at end of file diff --git a/app/src/main/java/space/taran/arkrate/data/crypto/CryptoRatesResponse.kt b/app/src/main/java/space/taran/arkrate/data/crypto/CryptoRatesResponse.kt new file mode 100644 index 000000000..9812e0ec0 --- /dev/null +++ b/app/src/main/java/space/taran/arkrate/data/crypto/CryptoRatesResponse.kt @@ -0,0 +1,6 @@ +package space.taran.arkrate.data.crypto + +data class CryptoRateResponse( + val symbol: String, + val price: Double +) \ 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 new file mode 100644 index 000000000..78b0212d8 --- /dev/null +++ b/app/src/main/java/space/taran/arkrate/data/db/AssetsDao.kt @@ -0,0 +1,41 @@ +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.data.CurrencyAmount +import javax.inject.Inject + +@Entity +data class RoomCurrencyAmount( + @PrimaryKey + val code: String, + val amount: Double +) + +@Dao +interface AssetsDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(currencyAmount: RoomCurrencyAmount) + + @Query("SELECT * FROM RoomCurrencyAmount") + suspend fun getAll(): List + + @Query("DELETE FROM RoomCurrencyAmount where code = :code") + suspend fun delete(code: String) +} + +class AssetsLocalDataSource @Inject constructor(val dao: AssetsDao) { + suspend fun insert(currencyAmount: CurrencyAmount) = + dao.insert(currencyAmount.toRoom()) + + suspend fun getAll() = dao.getAll().map { it.toCurrencyAmount() } + + suspend fun delete(code: String) = dao.delete(code) +} + +private fun RoomCurrencyAmount.toCurrencyAmount() = CurrencyAmount(code, amount) +private fun CurrencyAmount.toRoom() = RoomCurrencyAmount(code, amount) \ No newline at end of file diff --git a/app/src/main/java/space/taran/arkrate/data/db/CurrencyRateDao.kt b/app/src/main/java/space/taran/arkrate/data/db/CurrencyRateDao.kt new file mode 100644 index 000000000..114942154 --- /dev/null +++ b/app/src/main/java/space/taran/arkrate/data/db/CurrencyRateDao.kt @@ -0,0 +1,42 @@ +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.data.CurrencyRate +import space.taran.arkrate.data.CurrencyType +import javax.inject.Inject + +@Entity +data class RoomCurrencyRate( + @PrimaryKey + val code: String, + val currencyType: String, + val rate: Double +) + +@Dao +interface CurrencyRateDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(currencyRate: List) + + @Query("SELECT * FROM RoomCurrencyRate where currencyType = :currencyType") + suspend fun getByType(currencyType: String): List +} + +class CurrencyRateLocalDataSource @Inject constructor(val dao: CurrencyRateDao) { + suspend fun insert( + currencyRate: List, + currencyType: CurrencyType + ) = dao.insert(currencyRate.map { it.toRoom(currencyType) }) + + suspend fun getByType(currencyType: CurrencyType) = + dao.getByType(currencyType.name).map { it.toCurrencyRate() } +} + +private fun RoomCurrencyRate.toCurrencyRate() = CurrencyRate(code, rate) +private fun CurrencyRate.toRoom(currencyType: CurrencyType) = + RoomCurrencyRate(code, currencyType.name, rate) \ No newline at end of file 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 new file mode 100644 index 000000000..ce7bcd460 --- /dev/null +++ b/app/src/main/java/space/taran/arkrate/data/db/Database.kt @@ -0,0 +1,22 @@ +package space.taran.arkrate.data.db + +import androidx.room.RoomDatabase + +@androidx.room.Database( + entities = [ + RoomCurrencyAmount::class, + RoomCurrencyRate::class, + RoomFetchTimestamp::class + ], + version = 1, + exportSchema = false +) +abstract class Database : RoomDatabase() { + abstract fun assetsDao(): AssetsDao + abstract fun rateDao(): CurrencyRateDao + abstract fun fetchTimestampDao(): FetchTimestampDao + + companion object { + const val DB_NAME = "arkrate.db" + } +} \ No newline at end of file diff --git a/app/src/main/java/space/taran/arkrate/data/db/FetchTimestampDao.kt b/app/src/main/java/space/taran/arkrate/data/db/FetchTimestampDao.kt new file mode 100644 index 000000000..e0cba28b3 --- /dev/null +++ b/app/src/main/java/space/taran/arkrate/data/db/FetchTimestampDao.kt @@ -0,0 +1,35 @@ +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.data.CurrencyType +import javax.inject.Inject + +@Entity +data class RoomFetchTimestamp( + @PrimaryKey + val currencyType: String, + val timestamp: Long +) + +@Dao +interface FetchTimestampDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(fetchTimestamp: RoomFetchTimestamp) + + @Query("SELECT * FROM RoomFetchTimestamp where currencyType = :currencyType") + suspend fun getTimestamp(currencyType: String): RoomFetchTimestamp? +} + +class FetchTimestampDataSource @Inject constructor(private val dao: FetchTimestampDao) { + suspend fun rememberTimestamp(currencyType: CurrencyType) = + dao.insert(RoomFetchTimestamp(currencyType.name, System.currentTimeMillis())) + + suspend fun getTimestamp(currencyType: CurrencyType) = + dao.getTimestamp(currencyType.name)?.timestamp +} + diff --git a/app/src/main/java/space/taran/arkrate/data/fiat/FiatAPI.kt b/app/src/main/java/space/taran/arkrate/data/fiat/FiatAPI.kt new file mode 100644 index 000000000..9f6024b1f --- /dev/null +++ b/app/src/main/java/space/taran/arkrate/data/fiat/FiatAPI.kt @@ -0,0 +1,8 @@ +package space.taran.arkrate.data.fiat + +import retrofit2.http.GET + +interface FiatAPI { + @GET("/ARK-Builders/ark-exchange-rates/main/fiat-rates.json") + suspend fun get(): FiatRateResponse +} \ No newline at end of file diff --git a/app/src/main/java/space/taran/arkrate/data/fiat/FiatCurrencyRepo.kt b/app/src/main/java/space/taran/arkrate/data/fiat/FiatCurrencyRepo.kt new file mode 100644 index 000000000..4727de658 --- /dev/null +++ b/app/src/main/java/space/taran/arkrate/data/fiat/FiatCurrencyRepo.kt @@ -0,0 +1,204 @@ +package space.taran.arkrate.data.fiat + +import space.taran.arkrate.data.CurrencyName +import space.taran.arkrate.data.CurrencyRate +import space.taran.arkrate.data.CurrencyRepo +import space.taran.arkrate.data.CurrencyType +import space.taran.arkrate.data.network.NetworkStatus +import space.taran.arkrate.data.db.CurrencyRateLocalDataSource +import space.taran.arkrate.data.db.FetchTimestampDataSource +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class FiatCurrencyRepo @Inject constructor( + private val fiatAPI: FiatAPI, + private val local: CurrencyRateLocalDataSource, + private val networkStatus: NetworkStatus, + private val fetchTimestampDataSource: FetchTimestampDataSource +) : CurrencyRepo(local, networkStatus, fetchTimestampDataSource) { + override val type = CurrencyType.FIAT + + override suspend fun fetchRemote(): List = + fiatAPI.get().rates.map { (code, rate) -> + CurrencyRate(code, 1.0 / rate) + } + + override suspend fun getCurrencyName(): List = + getCurrencyRate().map { + CurrencyName(it.code, fiatCodeToCurrency[it.code]!!) + } +} + +private val fiatCodeToCurrency = mutableMapOf( + "AED" to "United Arab Emirates Dirham", + "AFN" to "Afghan Afghani", + "ALL" to "Albanian Lek", + "AMD" to "Armenian Dram", + "ANG" to "Netherlands Antillean Guilder", + "AOA" to "Angolan Kwanza", + "ARS" to "Argentine Peso", + "AUD" to "Australian Dollar", + "AWG" to "Aruban Florin", + "AZN" to "Azerbaijani Manat", + "BAM" to "Bosnia-Herzegovina Convertible Mark", + "BBD" to "Barbadian Dollar", + "BDT" to "Bangladeshi Taka", + "BGN" to "Bulgarian Lev", + "BHD" to "Bahraini Dinar", + "BIF" to "Burundian Franc", + "BMD" to "Bermudan Dollar", + "BND" to "Brunei Dollar", + "BOB" to "Bolivian Boliviano", + "BRL" to "Brazilian Real", + "BSD" to "Bahamian Dollar", + "BTC" to "Bitcoin", + "BTN" to "Bhutanese Ngultrum", + "BWP" to "Botswanan Pula", + "BYN" to "Belarusian Ruble", + "BZD" to "Belize Dollar", + "CAD" to "Canadian Dollar", + "CDF" to "Congolese Franc", + "CHF" to "Swiss Franc", + "CLF" to "Chilean Unit of Account (UF)", + "CLP" to "Chilean Peso", + "CNH" to "Chinese Yuan (Offshore)", + "CNY" to "Chinese Yuan", + "COP" to "Colombian Peso", + "CRC" to "Costa Rican Colón", + "CUC" to "Cuban Convertible Peso", + "CUP" to "Cuban Peso", + "CVE" to "Cape Verdean Escudo", + "CZK" to "Czech Republic Koruna", + "DJF" to "Djiboutian Franc", + "DKK" to "Danish Krone", + "DOP" to "Dominican Peso", + "DZD" to "Algerian Dinar", + "EGP" to "Egyptian Pound", + "ERN" to "Eritrean Nakfa", + "ETB" to "Ethiopian Birr", + "EUR" to "Euro", + "FJD" to "Fijian Dollar", + "FKP" to "Falkland Islands Pound", + "GBP" to "British Pound Sterling", + "GEL" to "Georgian Lari", + "GGP" to "Guernsey Pound", + "GHS" to "Ghanaian Cedi", + "GIP" to "Gibraltar Pound", + "GMD" to "Gambian Dalasi", + "GNF" to "Guinean Franc", + "GTQ" to "Guatemalan Quetzal", + "GYD" to "Guyanaese Dollar", + "HKD" to "Hong Kong Dollar", + "HNL" to "Honduran Lempira", + "HRK" to "Croatian Kuna", + "HTG" to "Haitian Gourde", + "HUF" to "Hungarian Forint", + "IDR" to "Indonesian Rupiah", + "ILS" to "Israeli New Sheqel", + "IMP" to "Manx pound", + "INR" to "Indian Rupee", + "IQD" to "Iraqi Dinar", + "IRR" to "Iranian Rial", + "ISK" to "Icelandic Króna", + "JEP" to "Jersey Pound", + "JMD" to "Jamaican Dollar", + "JOD" to "Jordanian Dinar", + "JPY" to "Japanese Yen", + "KES" to "Kenyan Shilling", + "KGS" to "Kyrgystani Som", + "KHR" to "Cambodian Riel", + "KMF" to "Comorian Franc", + "KPW" to "North Korean Won", + "KRW" to "South Korean Won", + "KWD" to "Kuwaiti Dinar", + "KYD" to "Cayman Islands Dollar", + "KZT" to "Kazakhstani Tenge", + "LAK" to "Laotian Kip", + "LBP" to "Lebanese Pound", + "LKR" to "Sri Lankan Rupee", + "LRD" to "Liberian Dollar", + "LSL" to "Lesotho Loti", + "LYD" to "Libyan Dinar", + "MAD" to "Moroccan Dirham", + "MDL" to "Moldovan Leu", + "MGA" to "Malagasy Ariary", + "MKD" to "Macedonian Denar", + "MMK" to "Myanma Kyat", + "MNT" to "Mongolian Tugrik", + "MOP" to "Macanese Pataca", + "MRU" to "Mauritanian Ouguiya", + "MUR" to "Mauritian Rupee", + "MVR" to "Maldivian Rufiyaa", + "MWK" to "Malawian Kwacha", + "MXN" to "Mexican Peso", + "MYR" to "Malaysian Ringgit", + "MZN" to "Mozambican Metical", + "NAD" to "Namibian Dollar", + "NGN" to "Nigerian Naira", + "NIO" to "Nicaraguan Córdoba", + "NOK" to "Norwegian Krone", + "NPR" to "Nepalese Rupee", + "NZD" to "New Zealand Dollar", + "OMR" to "Omani Rial", + "PAB" to "Panamanian Balboa", + "PEN" to "Peruvian Nuevo Sol", + "PGK" to "Papua New Guinean Kina", + "PHP" to "Philippine Peso", + "PKR" to "Pakistani Rupee", + "PLN" to "Polish Zloty", + "PYG" to "Paraguayan Guarani", + "QAR" to "Qatari Rial", + "RON" to "Romanian Leu", + "RSD" to "Serbian Dinar", + "RUB" to "Russian Ruble", + "RWF" to "Rwandan Franc", + "SAR" to "Saudi Riyal", + "SBD" to "Solomon Islands Dollar", + "SCR" to "Seychellois Rupee", + "SDG" to "Sudanese Pound", + "SEK" to "Swedish Krona", + "SGD" to "Singapore Dollar", + "SHP" to "Saint Helena Pound", + "SLL" to "Sierra Leonean Leone", + "SOS" to "Somali Shilling", + "SRD" to "Surinamese Dollar", + "SSP" to "South Sudanese Pound", + "STD" to "São Tomé and Príncipe Dobra (pre-2018)", + "STN" to "São Tomé and Príncipe Dobra", + "SVC" to "Salvadoran Colón", + "SYP" to "Syrian Pound", + "SZL" to "Swazi Lilangeni", + "THB" to "Thai Baht", + "TJS" to "Tajikistani Somoni", + "TMT" to "Turkmenistani Manat", + "TND" to "Tunisian Dinar", + "TOP" to "Tongan Pa'anga", + "TRY" to "Turkish Lira", + "TTD" to "Trinidad and Tobago Dollar", + "TWD" to "New Taiwan Dollar", + "TZS" to "Tanzanian Shilling", + "UAH" to "Ukrainian Hryvnia", + "UGX" to "Ugandan Shilling", + "USD" to "United States Dollar", + "UYU" to "Uruguayan Peso", + "UZS" to "Uzbekistan Som", + "VEF" to "Venezuelan Bolívar Fuerte (Old)", + "VES" to "Venezuelan Bolívar Soberano", + "VND" to "Vietnamese Dong", + "VUV" to "Vanuatu Vatu", + "WST" to "Samoan Tala", + "XAF" to "CFA Franc BEAC", + "XAG" to "Silver Ounce", + "XAU" to "Gold Ounce", + "XCD" to "East Caribbean Dollar", + "XDR" to "Special Drawing Rights", + "XOF" to "CFA Franc BCEAO", + "XPD" to "Palladium Ounce", + "XPF" to "CFP Franc", + "XPT" to "Platinum Ounce", + "YER" to "Yemeni Rial", + "ZAR" to "South African Rand", + "ZMW" to "Zambian Kwacha", + "ZWL" to "Zimbabwean Dollar" +) \ No newline at end of file diff --git a/app/src/main/java/space/taran/arkrate/data/fiat/FiatRateResponse.kt b/app/src/main/java/space/taran/arkrate/data/fiat/FiatRateResponse.kt new file mode 100644 index 000000000..81d35079a --- /dev/null +++ b/app/src/main/java/space/taran/arkrate/data/fiat/FiatRateResponse.kt @@ -0,0 +1,6 @@ +package space.taran.arkrate.data.fiat + +data class FiatRateResponse( + val timestamp: Long, + val rates: Map +) \ No newline at end of file diff --git a/app/src/main/java/space/taran/arkrate/data/network/NetworkStatus.kt b/app/src/main/java/space/taran/arkrate/data/network/NetworkStatus.kt new file mode 100644 index 000000000..2ebc5166a --- /dev/null +++ b/app/src/main/java/space/taran/arkrate/data/network/NetworkStatus.kt @@ -0,0 +1,30 @@ +package space.taran.arkrate.data.network + +import android.content.Context +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities +import android.os.Build +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class NetworkStatus @Inject constructor(private val context: Context) { + private val cm = + context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + + fun isOnline(): Boolean { + val network: Network = cm.activeNetwork ?: return false + val networkCapabilities: NetworkCapabilities = + cm.getNetworkCapabilities(network) ?: return false + + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) + && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED) + } else { + networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/space/taran/arkrate/data/network/OkHttpClientBuilder.kt b/app/src/main/java/space/taran/arkrate/data/network/OkHttpClientBuilder.kt new file mode 100644 index 000000000..cfbba7e5b --- /dev/null +++ b/app/src/main/java/space/taran/arkrate/data/network/OkHttpClientBuilder.kt @@ -0,0 +1,36 @@ +package space.taran.arkrate.data.network + +import android.content.Context +import android.webkit.WebSettings +import okhttp3.CipherSuite +import okhttp3.ConnectionSpec +import okhttp3.OkHttpClient +import okhttp3.TlsVersion +import okhttp3.logging.HttpLoggingInterceptor +import java.security.SecureRandom +import java.security.cert.X509Certificate +import java.util.Collections +import javax.inject.Inject +import javax.inject.Singleton +import javax.net.ssl.SSLContext +import javax.net.ssl.X509TrustManager + +@Singleton +class OkHttpClientBuilder @Inject constructor(val context: Context) { + fun build(): OkHttpClient { + val agent = WebSettings.getDefaultUserAgent(context) + + val client = OkHttpClient.Builder() + .addNetworkInterceptor { chain -> + chain.proceed( + chain.request() + .newBuilder() + .header("User-Agent", agent) + .build() + ) + } + .build() + + return client + } +} \ 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 new file mode 100644 index 000000000..327c29476 --- /dev/null +++ b/app/src/main/java/space/taran/arkrate/di/AppComponent.kt @@ -0,0 +1,37 @@ +package space.taran.arkrate.di + +import android.app.Application +import android.content.Context +import dagger.BindsInstance +import dagger.Component +import space.taran.arkrate.data.GeneralCurrencyRepo +import space.taran.arkrate.data.assets.AssetsRepo +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 javax.inject.Singleton + +@Singleton +@Component( + modules = [ + ApiModule::class, + DBModule::class + ] +) +interface AppComponent { + fun summaryViewModelFactory(): SummaryViewModelFactory + fun assetsVMFactory(): AssetsViewModelFactory + fun addCurrencyVMFactory(): AddCurrencyViewModelFactory + fun generalCurrencyRepo(): GeneralCurrencyRepo + fun assetsRepo(): AssetsRepo + + @Component.Factory + interface Factory { + fun create( + @BindsInstance application: Application, + @BindsInstance context: Context + ): AppComponent + } +} \ No newline at end of file diff --git a/app/src/main/java/space/taran/arkrate/di/DIManager.kt b/app/src/main/java/space/taran/arkrate/di/DIManager.kt new file mode 100644 index 000000000..d264925cc --- /dev/null +++ b/app/src/main/java/space/taran/arkrate/di/DIManager.kt @@ -0,0 +1,12 @@ +package space.taran.arkrate.di + +import android.app.Application + +object DIManager { + lateinit var component: AppComponent + private set + + fun init(app: Application) { + component = DaggerAppComponent.factory().create(app, app.applicationContext) + } +} \ No newline at end of file diff --git a/app/src/main/java/space/taran/arkrate/di/module/ApiModule.kt b/app/src/main/java/space/taran/arkrate/di/module/ApiModule.kt new file mode 100644 index 000000000..a22ce3bd6 --- /dev/null +++ b/app/src/main/java/space/taran/arkrate/di/module/ApiModule.kt @@ -0,0 +1,44 @@ +package space.taran.arkrate.di.module + +import com.google.gson.GsonBuilder +import dagger.Module +import dagger.Provides +import okhttp3.OkHttpClient +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import space.taran.arkrate.data.crypto.CryptoAPI +import space.taran.arkrate.data.fiat.FiatAPI +import space.taran.arkrate.data.network.OkHttpClientBuilder +import javax.inject.Named +import javax.inject.Singleton + +@Module +class ApiModule { + @Singleton + @Provides + fun cryptoAPI(clientBuilder: OkHttpClientBuilder): CryptoAPI { + val httpClient = clientBuilder.build() + val gson = GsonBuilder().create() + + return Retrofit.Builder() + .baseUrl("https://raw.githubusercontent.com") + .addConverterFactory(GsonConverterFactory.create(gson)) + .client(httpClient) + .build() + .create(CryptoAPI::class.java) + } + + @Singleton + @Provides + fun fiatAPI(clientBuilder: OkHttpClientBuilder): FiatAPI { + val httpClient = clientBuilder.build() + val gson = GsonBuilder().create() + + return Retrofit.Builder() + .baseUrl("https://raw.githubusercontent.com") + .addConverterFactory(GsonConverterFactory.create(gson)) + .client(httpClient) + .build() + .create(FiatAPI::class.java) + } +} \ 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 new file mode 100644 index 000000000..f23a1d8c9 --- /dev/null +++ b/app/src/main/java/space/taran/arkrate/di/module/DBModule.kt @@ -0,0 +1,30 @@ +package space.taran.arkrate.di.module + +import android.app.Application +import androidx.room.Room +import dagger.Module +import dagger.Provides +import space.taran.arkrate.data.db.Database +import space.taran.arkrate.data.db.Database.Companion.DB_NAME +import javax.inject.Singleton + +@Module +class DBModule { + + @Singleton + @Provides + fun database(app: Application): Database { + return Room.databaseBuilder(app, Database::class.java, DB_NAME) + .fallbackToDestructiveMigration() + .build() + } + + @Provides + fun assetsDao(db: Database) = db.assetsDao() + + @Provides + fun rateDao(db: Database) = db.rateDao() + + @Provides + fun fetchTimestampDao(db: Database) = db.fetchTimestampDao() +} \ No newline at end of file diff --git a/app/src/main/java/space/taran/arkrate/network/crypto.kt b/app/src/main/java/space/taran/arkrate/network/crypto.kt deleted file mode 100644 index 89671e50d..000000000 --- a/app/src/main/java/space/taran/arkrate/network/crypto.kt +++ /dev/null @@ -1,74 +0,0 @@ -package space.taran.arkrate.network - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import com.squareup.moshi.Moshi -import com.squareup.moshi.Types -import okhttp3.OkHttpClient -import okhttp3.Request -import java.io.File - -@JsonClass(generateAdapter = true) -data class BinancePrizeItem( - @Json(name = "price") - val price: String, - @Json(name = "symbol") - val symbol: String -) - -@JsonClass(generateAdapter = true) -data class BinancePrizeTimestamp( - val timestamp: Int, - val binancePrize: Map -) - -class Crypto { - private fun getCryptoPrice(): Map { - - val client = OkHttpClient() - val request = Request.Builder() - .url("https://api.binance.com/api/v3/ticker/price") - .build() - - val response = client.newCall(request).execute().body!!.string() - val moshi: Moshi = Moshi.Builder().build() - val jsonAdapter = moshi.adapter>( - Types.newParameterizedType( - List::class.java, - BinancePrizeItem::class.java - ) - ) - - val json = jsonAdapter.fromJson(response) - val result = mutableMapOf() - json!!.forEach { - result[it.symbol] = it.price.toDouble() - } - return result - } - - fun getCryptoPriceWithCache(filePath: String): Map { - val file = File((File(filePath).parent ?: "") + "/CryptoCache.json") - val moshi: Moshi = Moshi.Builder().build() - val jsonAdapter = moshi.adapter(BinancePrizeTimestamp::class.java) - if (!file.canWrite()) { - val result = BinancePrizeTimestamp( - timestamp = (System.currentTimeMillis() / 1000).toInt(), - getCryptoPrice() - ) - file.writeText(jsonAdapter.toJson(result)) - return result.binancePrize - } - val origin = file.readText() - val json = jsonAdapter.fromJson(origin) - if ((json?.timestamp ?: 0) + 86400 < System.currentTimeMillis() / 1000) { - val result = BinancePrizeTimestamp( - timestamp = (System.currentTimeMillis() / 1000).toInt(), - getCryptoPrice() - ) - file.writeText(jsonAdapter.toJson(result)) - return result.binancePrize - } - return json!!.binancePrize - } -} \ No newline at end of file diff --git a/app/src/main/java/space/taran/arkrate/network/currencies.kt b/app/src/main/java/space/taran/arkrate/network/currencies.kt deleted file mode 100644 index f591b7202..000000000 --- a/app/src/main/java/space/taran/arkrate/network/currencies.kt +++ /dev/null @@ -1,192 +0,0 @@ -package space.taran.arkrate.network - - -object currencies { - val currencies: Map - get() { - val result = mutableMapOf( - "AED" to "United Arab Emirates Dirham", - "AFN" to "Afghan Afghani", - "ALL" to "Albanian Lek", - "AMD" to "Armenian Dram", - "ANG" to "Netherlands Antillean Guilder", - "AOA" to "Angolan Kwanza", - "ARS" to "Argentine Peso", - "AUD" to "Australian Dollar", - "AWG" to "Aruban Florin", - "AZN" to "Azerbaijani Manat", - "BAM" to "Bosnia-Herzegovina Convertible Mark", - "BBD" to "Barbadian Dollar", - "BDT" to "Bangladeshi Taka", - "BGN" to "Bulgarian Lev", - "BHD" to "Bahraini Dinar", - "BIF" to "Burundian Franc", - "BMD" to "Bermudan Dollar", - "BND" to "Brunei Dollar", - "BOB" to "Bolivian Boliviano", - "BRL" to "Brazilian Real", - "BSD" to "Bahamian Dollar", - "BTC" to "Bitcoin", - "BTN" to "Bhutanese Ngultrum", - "BWP" to "Botswanan Pula", - "BYN" to "Belarusian Ruble", - "BZD" to "Belize Dollar", - "CAD" to "Canadian Dollar", - "CDF" to "Congolese Franc", - "CHF" to "Swiss Franc", - "CLF" to "Chilean Unit of Account (UF)", - "CLP" to "Chilean Peso", - "CNH" to "Chinese Yuan (Offshore)", - "CNY" to "Chinese Yuan", - "COP" to "Colombian Peso", - "CRC" to "Costa Rican Colón", - "CUC" to "Cuban Convertible Peso", - "CUP" to "Cuban Peso", - "CVE" to "Cape Verdean Escudo", - "CZK" to "Czech Republic Koruna", - "DJF" to "Djiboutian Franc", - "DKK" to "Danish Krone", - "DOP" to "Dominican Peso", - "DZD" to "Algerian Dinar", - "EGP" to "Egyptian Pound", - "ERN" to "Eritrean Nakfa", - "ETB" to "Ethiopian Birr", - "EUR" to "Euro", - "FJD" to "Fijian Dollar", - "FKP" to "Falkland Islands Pound", - "GBP" to "British Pound Sterling", - "GEL" to "Georgian Lari", - "GGP" to "Guernsey Pound", - "GHS" to "Ghanaian Cedi", - "GIP" to "Gibraltar Pound", - "GMD" to "Gambian Dalasi", - "GNF" to "Guinean Franc", - "GTQ" to "Guatemalan Quetzal", - "GYD" to "Guyanaese Dollar", - "HKD" to "Hong Kong Dollar", - "HNL" to "Honduran Lempira", - "HRK" to "Croatian Kuna", - "HTG" to "Haitian Gourde", - "HUF" to "Hungarian Forint", - "IDR" to "Indonesian Rupiah", - "ILS" to "Israeli New Sheqel", - "IMP" to "Manx pound", - "INR" to "Indian Rupee", - "IQD" to "Iraqi Dinar", - "IRR" to "Iranian Rial", - "ISK" to "Icelandic Króna", - "JEP" to "Jersey Pound", - "JMD" to "Jamaican Dollar", - "JOD" to "Jordanian Dinar", - "JPY" to "Japanese Yen", - "KES" to "Kenyan Shilling", - "KGS" to "Kyrgystani Som", - "KHR" to "Cambodian Riel", - "KMF" to "Comorian Franc", - "KPW" to "North Korean Won", - "KRW" to "South Korean Won", - "KWD" to "Kuwaiti Dinar", - "KYD" to "Cayman Islands Dollar", - "KZT" to "Kazakhstani Tenge", - "LAK" to "Laotian Kip", - "LBP" to "Lebanese Pound", - "LKR" to "Sri Lankan Rupee", - "LRD" to "Liberian Dollar", - "LSL" to "Lesotho Loti", - "LYD" to "Libyan Dinar", - "MAD" to "Moroccan Dirham", - "MDL" to "Moldovan Leu", - "MGA" to "Malagasy Ariary", - "MKD" to "Macedonian Denar", - "MMK" to "Myanma Kyat", - "MNT" to "Mongolian Tugrik", - "MOP" to "Macanese Pataca", - "MRU" to "Mauritanian Ouguiya", - "MUR" to "Mauritian Rupee", - "MVR" to "Maldivian Rufiyaa", - "MWK" to "Malawian Kwacha", - "MXN" to "Mexican Peso", - "MYR" to "Malaysian Ringgit", - "MZN" to "Mozambican Metical", - "NAD" to "Namibian Dollar", - "NGN" to "Nigerian Naira", - "NIO" to "Nicaraguan Córdoba", - "NOK" to "Norwegian Krone", - "NPR" to "Nepalese Rupee", - "NZD" to "New Zealand Dollar", - "OMR" to "Omani Rial", - "PAB" to "Panamanian Balboa", - "PEN" to "Peruvian Nuevo Sol", - "PGK" to "Papua New Guinean Kina", - "PHP" to "Philippine Peso", - "PKR" to "Pakistani Rupee", - "PLN" to "Polish Zloty", - "PYG" to "Paraguayan Guarani", - "QAR" to "Qatari Rial", - "RON" to "Romanian Leu", - "RSD" to "Serbian Dinar", - "RUB" to "Russian Ruble", - "RWF" to "Rwandan Franc", - "SAR" to "Saudi Riyal", - "SBD" to "Solomon Islands Dollar", - "SCR" to "Seychellois Rupee", - "SDG" to "Sudanese Pound", - "SEK" to "Swedish Krona", - "SGD" to "Singapore Dollar", - "SHP" to "Saint Helena Pound", - "SLL" to "Sierra Leonean Leone", - "SOS" to "Somali Shilling", - "SRD" to "Surinamese Dollar", - "SSP" to "South Sudanese Pound", - "STD" to "São Tomé and Príncipe Dobra (pre-2018)", - "STN" to "São Tomé and Príncipe Dobra", - "SVC" to "Salvadoran Colón", - "SYP" to "Syrian Pound", - "SZL" to "Swazi Lilangeni", - "THB" to "Thai Baht", - "TJS" to "Tajikistani Somoni", - "TMT" to "Turkmenistani Manat", - "TND" to "Tunisian Dinar", - "TOP" to "Tongan Pa'anga", - "TRY" to "Turkish Lira", - "TTD" to "Trinidad and Tobago Dollar", - "TWD" to "New Taiwan Dollar", - "TZS" to "Tanzanian Shilling", - "UAH" to "Ukrainian Hryvnia", - "UGX" to "Ugandan Shilling", - "USD" to "United States Dollar", - "UYU" to "Uruguayan Peso", - "UZS" to "Uzbekistan Som", - "VEF" to "Venezuelan Bolívar Fuerte (Old)", - "VES" to "Venezuelan Bolívar Soberano", - "VND" to "Vietnamese Dong", - "VUV" to "Vanuatu Vatu", - "WST" to "Samoan Tala", - "XAF" to "CFA Franc BEAC", - "XAG" to "Silver Ounce", - "XAU" to "Gold Ounce", - "XCD" to "East Caribbean Dollar", - "XDR" to "Special Drawing Rights", - "XOF" to "CFA Franc BCEAO", - "XPD" to "Palladium Ounce", - "XPF" to "CFP Franc", - "XPT" to "Platinum Ounce", - "YER" to "Yemeni Rial", - "ZAR" to "South African Rand", - "ZMW" to "Zambian Kwacha", - "ZWL" to "Zimbabwean Dollar" - ) - - return result - } - - fun get(filePath: String): Map { - val result = currencies.toMutableMap() - Crypto().getCryptoPriceWithCache(filePath).keys.forEach { - if (it.takeLast(4) == "USDT") { - result[it.dropLast(4)] = "" - } - } - return result - } -} \ No newline at end of file diff --git a/app/src/main/java/space/taran/arkrate/network/rates.kt b/app/src/main/java/space/taran/arkrate/network/rates.kt deleted file mode 100644 index 276ca1315..000000000 --- a/app/src/main/java/space/taran/arkrate/network/rates.kt +++ /dev/null @@ -1,50 +0,0 @@ -package space.taran.arkrate.network - -import com.squareup.moshi.JsonClass -import com.squareup.moshi.Moshi -import okhttp3.OkHttpClient -import okhttp3.Request -import java.io.File - -@JsonClass(generateAdapter = true) -data class rates( - val base: String, - val disclaimer: String, - val license: String, - val rates: Map, - val timestamp: Int -) - -fun getAllRates(): rates { - val client = OkHttpClient() - val request = Request.Builder() - .url("https://raw.githubusercontent.com/ARK-Builders/ark-exchange-rates/main/rates.json") - .build() - - val response = client.newCall(request).execute().body!!.string() - - val moshi: Moshi = Moshi.Builder().build() - val jsonAdapter = moshi.adapter(rates::class.java) - val json = jsonAdapter.fromJson(response) - - return json!! -} - -fun getAllRatesWithCache(filePath: String): Map { - val file = File((File(filePath).parent ?: "") + "/rateCache.json") - val moshi: Moshi = Moshi.Builder().build() - val jsonAdapter = moshi.adapter(rates::class.java) - if (!file.canWrite()) { - val result = getAllRates() - file.writeText(jsonAdapter.toJson(result)) - return result.rates - } - val origin = file.readText() - val json = jsonAdapter.fromJson(origin) - if ((json?.timestamp ?: 0) + 86400 < System.currentTimeMillis() / 1000) { - val result = getAllRates() - file.writeText(jsonAdapter.toJson(result)) - return result.rates - } - return json!!.rates -} diff --git a/app/src/main/java/space/taran/arkrate/App.kt b/app/src/main/java/space/taran/arkrate/presentation/App.kt similarity index 87% rename from app/src/main/java/space/taran/arkrate/App.kt rename to app/src/main/java/space/taran/arkrate/presentation/App.kt index 238e28d5f..f1b3e626d 100644 --- a/app/src/main/java/space/taran/arkrate/App.kt +++ b/app/src/main/java/space/taran/arkrate/presentation/App.kt @@ -1,4 +1,4 @@ -package space.taran.arkrate +package space.taran.arkrate.presentation import android.app.Application import kotlinx.coroutines.CoroutineScope @@ -9,6 +9,9 @@ import org.acra.config.httpSender import org.acra.data.StringFormat import org.acra.ktx.initAcra import org.acra.sender.HttpSender +import space.taran.arkrate.BuildConfig +import space.taran.arkrate.R +import space.taran.arkrate.di.DIManager import space.taran.arkrate.utils.Config class App: Application() { @@ -16,6 +19,7 @@ class App: Application() { override fun onCreate() { super.onCreate() initAcra() + DIManager.init(this) } private fun initAcra() = CoroutineScope(Dispatchers.IO).launch { diff --git a/app/src/main/java/space/taran/arkrate/MainActivity.kt b/app/src/main/java/space/taran/arkrate/presentation/MainActivity.kt similarity index 59% rename from app/src/main/java/space/taran/arkrate/MainActivity.kt rename to app/src/main/java/space/taran/arkrate/presentation/MainActivity.kt index 857b57d22..1935cb17d 100644 --- a/app/src/main/java/space/taran/arkrate/MainActivity.kt +++ b/app/src/main/java/space/taran/arkrate/presentation/MainActivity.kt @@ -1,20 +1,19 @@ -package space.taran.arkrate +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 space.taran.arkrate.ui.Create +import space.taran.arkrate.presentation.theme.ARKRateTheme class MainActivity : ComponentActivity() { - @RequiresApi(Build.VERSION_CODES.O) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { - Create( - filePath = getExternalFilesDir("database")?.absolutePath.toString() + "/Currencies.json" - ) + ARKRateTheme { + MainScreen() + } } } } \ No newline at end of file diff --git a/app/src/main/java/space/taran/arkrate/presentation/MainScreen.kt b/app/src/main/java/space/taran/arkrate/presentation/MainScreen.kt new file mode 100644 index 000000000..ef48cf3a3 --- /dev/null +++ b/app/src/main/java/space/taran/arkrate/presentation/MainScreen.kt @@ -0,0 +1,48 @@ +package space.taran.arkrate.presentation + +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +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) +@Composable +fun MainScreen() { + val navController = rememberAnimatedNavController() + AnimatedNavHost(navController, startDestination = Screen.Assets.name) { + composable(Screen.Assets.name) { + AssetsScreen(navController) + } + composable( + Screen.AddCurrency.name, + enterTransition = { + fadeIn() + }, + exitTransition = { + fadeOut() + } + ) { + AddCurrencyScreen(navController) + } + composable(Screen.Summary.name, + enterTransition = { + fadeIn() + }, + exitTransition = { + fadeOut() + } + ) { + SummaryScreen() + } + } +} + +enum class Screen { + Assets, AddCurrency, Summary +} \ No newline at end of file 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 new file mode 100644 index 000000000..abbfbf5ca --- /dev/null +++ b/app/src/main/java/space/taran/arkrate/presentation/addcurrency/AddCurrencyScreen.kt @@ -0,0 +1,78 @@ +package space.taran.arkrate.presentation.addcurrency + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.Divider +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import space.taran.arkrate.di.DIManager + +@Composable +fun AddCurrencyScreen(navController: NavController) { + val viewModel: AddCurrencyViewModel = + viewModel(factory = DIManager.component.addCurrencyVMFactory()) + var filter by remember { mutableStateOf("") } + val filteredCurrencyNameList = viewModel.currencyNameList?.filter { (code, _) -> + code.startsWith(filter.uppercase()) + } ?: emptyList() + + Column(Modifier.fillMaxSize()) { + Row { + OutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .padding(start = 20.dp, end = 20.dp, top = 16.dp), + value = filter, + onValueChange = { filter = it }, + label = { + Text("Search") + } + ) + } + LazyColumn { + items(filteredCurrencyNameList.sortedBy { it.code }) { currencyName -> + CurrencyItem( + code = currencyName.code, + currency = currencyName.name, + onAdd = { + viewModel.addCurrency(currencyName.code) + navController.popBackStack() + } + ) + } + } + } +} + +@Composable +private fun CurrencyItem(code: String, currency: String, onAdd: () -> Unit) { + Column( + Modifier + .fillMaxWidth() + .clickable(onClick = { onAdd() }) + .padding(horizontal = 20.dp) + ) { + Spacer(modifier = Modifier.height(2.dp)) + Box(Modifier.fillMaxWidth()) { + Text( + modifier = Modifier.align(Alignment.CenterStart), + text = code, + fontSize = 20.sp + ) + Text( + modifier = Modifier.align(Alignment.CenterEnd), + text = currency, + ) + } + Divider() + } +} \ No newline at end of file diff --git a/app/src/main/java/space/taran/arkrate/presentation/addcurrency/AddCurrencyViewModel.kt b/app/src/main/java/space/taran/arkrate/presentation/addcurrency/AddCurrencyViewModel.kt new file mode 100644 index 000000000..42eea8864 --- /dev/null +++ b/app/src/main/java/space/taran/arkrate/presentation/addcurrency/AddCurrencyViewModel.kt @@ -0,0 +1,41 @@ +package space.taran.arkrate.presentation.addcurrency + +import androidx.compose.runtime.getValue +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.CurrencyName +import space.taran.arkrate.data.GeneralCurrencyRepo +import space.taran.arkrate.data.assets.AssetsRepo +import javax.inject.Inject +import javax.inject.Singleton + +class AddCurrencyViewModel( + private val assetsRepo: AssetsRepo, + private val currencyRepo: GeneralCurrencyRepo +): ViewModel() { + var currencyNameList by mutableStateOf?>(null) + + init { + viewModelScope.launch { + currencyNameList = currencyRepo.getCurrencyName() + } + } + + fun addCurrency(code: String) = viewModelScope.launch { + assetsRepo.setCurrencyAmount(code, 0.0) + } +} + +@Singleton +class AddCurrencyViewModelFactory @Inject constructor( + private val assetsRepo: AssetsRepo, + private val currencyRepo: GeneralCurrencyRepo +): ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return AddCurrencyViewModel(assetsRepo, currencyRepo) as T + } +} \ No newline at end of file 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 new file mode 100644 index 000000000..626008eb6 --- /dev/null +++ b/app/src/main/java/space/taran/arkrate/presentation/assets/AssetsScreen.kt @@ -0,0 +1,120 @@ +package space.taran.arkrate.presentation.assets + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.FloatingActionButton +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.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 +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +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 space.taran.arkrate.data.CurrencyAmount +import space.taran.arkrate.di.DIManager +import space.taran.arkrate.presentation.Screen +import space.taran.arkrate.utils.removeFractionalPartIfEmpty + +@Composable +fun AssetsScreen(navController: NavController) { + val viewModel: AssetsViewModel = + viewModel(factory = DIManager.component.assetsVMFactory()) + + Box(Modifier.fillMaxSize()) { + LazyColumn(modifier = Modifier.fillMaxSize()) { + items( + viewModel.currencyAmountList, + key = { it.code } + ) { currencyAmount -> + CurrencyEditItem( + modifier = Modifier, + currencyAmount, + viewModel + ) + } + } + FloatingActionButton( + modifier = Modifier + .align(Alignment.BottomStart) + .padding(10.dp), + onClick = { navController.navigate(Screen.AddCurrency.name) }, + ) { + 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") + } + } +} + +@Composable +private fun CurrencyEditItem( + modifier: Modifier, + currencyAmount: CurrencyAmount, + viewModel: AssetsViewModel, +) { + val code = currencyAmount.code + var amountInput by remember { + mutableStateOf( + if (currencyAmount.amount == 0.0) "" + else currencyAmount.amount.removeFractionalPartIfEmpty() + ) + } + val clearIcon = @Composable { + IconButton(onClick = { + amountInput = + viewModel.onAmountChanged(code, amountInput, "") + }) { + Icon( + Icons.Default.Clear, + contentDescription = "" + ) + } + } + Row(modifier.padding(horizontal = 20.dp, vertical = 4.dp)) { + OutlinedTextField( + modifier = Modifier.weight(1f), + value = amountInput, + onValueChange = { newInput -> + amountInput = viewModel.onAmountChanged( + code, + amountInput, + newInput + ) + }, + trailingIcon = clearIcon, + label = { Text(code) }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + maxLines = 1, + ) + IconButton( + modifier = Modifier.padding(8.dp), + onClick = { viewModel.onCurrencyRemoved(code) } + ) { + Icon(Icons.Filled.Delete, "Delete") + } + } +} diff --git a/app/src/main/java/space/taran/arkrate/presentation/assets/AssetsViewModel.kt b/app/src/main/java/space/taran/arkrate/presentation/assets/AssetsViewModel.kt new file mode 100644 index 000000000..f0864e457 --- /dev/null +++ b/app/src/main/java/space/taran/arkrate/presentation/assets/AssetsViewModel.kt @@ -0,0 +1,61 @@ +package space.taran.arkrate.presentation.assets + +import androidx.compose.runtime.mutableStateListOf +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.launch +import space.taran.arkrate.data.CurrencyAmount +import space.taran.arkrate.data.assets.AssetsRepo +import javax.inject.Inject +import javax.inject.Singleton + +class AssetsViewModel( + private val assetsRepo: AssetsRepo +): ViewModel() { + var currencyAmountList = mutableStateListOf() + + init { + viewModelScope.launch { + assetsRepo.allCurrencyAmountFlow().collect { + currencyAmountList.clear() + currencyAmountList.addAll(it) + } + } + } + + fun onAmountChanged(code: String, 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)) { + viewModelScope.launch { + assetsRepo.setCurrencyAmount(code, 0.0) + } + return newInput + } + + viewModelScope.launch { + assetsRepo.setCurrencyAmount(code, newInput.toDouble()) + } + + val leadingZeros = "^0+(?=\\d)".toRegex() + + return newInput.replace(leadingZeros,"") + } + + fun onCurrencyRemoved(code: String) = viewModelScope.launch { + assetsRepo.removeCurrency(code) + } +} + +@Singleton +class AssetsViewModelFactory @Inject constructor( + private val assetsRepo: AssetsRepo +): ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return AssetsViewModel(assetsRepo) 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 new file mode 100644 index 000000000..efc0ee98f --- /dev/null +++ b/app/src/main/java/space/taran/arkrate/presentation/summary/SummaryScreen.kt @@ -0,0 +1,122 @@ +package space.taran.arkrate.presentation.summary + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.Card +import androidx.compose.material.Divider +import androidx.compose.material.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.viewmodel.compose.viewModel +import space.taran.arkrate.di.DIManager +import java.math.RoundingMode +import java.text.DecimalFormat + +private val format = DecimalFormat("0.######").apply { + roundingMode = RoundingMode.HALF_DOWN +} + +@Composable +fun SummaryScreen() { + val viewModel: SummaryViewModel = + viewModel(factory = DIManager.component.summaryViewModelFactory()) + Box(Modifier.fillMaxSize()) { + LazyColumn(modifier = Modifier.align(Alignment.Center)) { + item { + TotalCard(viewModel) + } + item { + ExchangeCard(viewModel) + } + } + } +} + +@Composable +private fun TotalCard(viewModel: SummaryViewModel) { + val total by viewModel.total.collectAsState() + total ?: return + var visible by remember { mutableStateOf(true) } + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 15.dp) + .clickable { + visible = !visible + }, + elevation = 8.dp + ) { + Column(modifier = Modifier.padding(8.dp)) { + Text("TOTAL", fontSize = 24.sp) + Divider() + AnimatedVisibility(visible) { + Column { + total!!.forEach { + Box( + modifier = Modifier + .fillMaxWidth() + .height(32.dp) + ) { + Text( + it.key, + modifier = Modifier.align(Alignment.CenterStart) + ) + Text( + format.format(it.value), + modifier = Modifier.align(Alignment.CenterEnd) + ) + Divider() + } + } + } + } + } + } +} + +@Composable +private fun ExchangeCard(viewModel: SummaryViewModel) { + val exchange by viewModel.exchange.collectAsState() + exchange ?: return + var visible by remember { mutableStateOf(true) } + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 15.dp) + .clickable { + visible = !visible + }, + elevation = 8.dp + ) { + Column(modifier = Modifier.padding(8.dp)) { + Text("EXCHANGE", fontSize = 24.sp) + Divider() + AnimatedVisibility(visible) { + Column { + exchange!!.forEach { + Box( + modifier = Modifier + .fillMaxWidth() + .height(32.dp) + ) { + Text( + it.key, + modifier = Modifier.align(Alignment.CenterStart) + ) + Text( + format.format(it.value), + modifier = Modifier.align(Alignment.CenterEnd) + ) + Divider() + } + } + } + } + } + } +} \ No newline at end of file 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 new file mode 100644 index 000000000..4b2aa0211 --- /dev/null +++ b/app/src/main/java/space/taran/arkrate/presentation/summary/SummaryViewModel.kt @@ -0,0 +1,73 @@ +package space.taran.arkrate.presentation.summary + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +import space.taran.arkrate.data.GeneralCurrencyRepo +import space.taran.arkrate.data.assets.AssetsRepo +import javax.inject.Inject +import javax.inject.Singleton + +class SummaryViewModel( + private val assetsRepo: AssetsRepo, + private val currencyRepo: GeneralCurrencyRepo +) : ViewModel() { + var total = MutableStateFlow?>(null) + var exchange = MutableStateFlow?>(null) + + init { + viewModelScope.launch { + calculateTotal() + calculateExchange() + } + } + + private suspend fun calculateTotal() { + val count = assetsRepo.allCurrencyAmount().map { + it.code to it.amount + }.toMap() + val rates = currencyRepo.getCurrencyRate().associate { it.code to it.rate } + val result = mutableMapOf() + var USD = 0.0 + + count.forEach { + USD += it.value * rates[it.key]!! + } + + count.forEach { + result[it.key] = USD / rates[it.key]!! + } + + total.emit(result) + } + + private suspend fun calculateExchange() { + val count = assetsRepo.allCurrencyAmount().map { + it.code to it.amount + }.toMap() + val rates = currencyRepo.getCurrencyRate().associate { it.code to it.rate } + val result = mutableMapOf() + + count.forEach { i -> + count.forEach { j -> + if (j.key != i.key) { + result["${i.key}/${j.key}"] = rates[i.key]!! / rates[j.key]!! + } + } + } + + exchange.emit(result) + } +} + +@Singleton +class SummaryViewModelFactory @Inject constructor( + private val assetsRepo: AssetsRepo, + private val currencyRepo: GeneralCurrencyRepo +) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return SummaryViewModel(assetsRepo, currencyRepo) as T + } +} \ No newline at end of file diff --git a/app/src/main/java/space/taran/arkrate/ui/theme/Color.kt b/app/src/main/java/space/taran/arkrate/presentation/theme/Color.kt similarity index 79% rename from app/src/main/java/space/taran/arkrate/ui/theme/Color.kt rename to app/src/main/java/space/taran/arkrate/presentation/theme/Color.kt index 66982b5cf..c54c483ea 100644 --- a/app/src/main/java/space/taran/arkrate/ui/theme/Color.kt +++ b/app/src/main/java/space/taran/arkrate/presentation/theme/Color.kt @@ -1,4 +1,4 @@ -package space.taran.arkrate.ui.theme +package space.taran.arkrate.presentation.theme import androidx.compose.ui.graphics.Color diff --git a/app/src/main/java/space/taran/arkrate/ui/theme/Shape.kt b/app/src/main/java/space/taran/arkrate/presentation/theme/Shape.kt similarity index 85% rename from app/src/main/java/space/taran/arkrate/ui/theme/Shape.kt rename to app/src/main/java/space/taran/arkrate/presentation/theme/Shape.kt index 49cc592e1..8c89adcbe 100644 --- a/app/src/main/java/space/taran/arkrate/ui/theme/Shape.kt +++ b/app/src/main/java/space/taran/arkrate/presentation/theme/Shape.kt @@ -1,4 +1,4 @@ -package space.taran.arkrate.ui.theme +package space.taran.arkrate.presentation.theme import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Shapes diff --git a/app/src/main/java/space/taran/arkrate/ui/theme/Theme.kt b/app/src/main/java/space/taran/arkrate/presentation/theme/Theme.kt similarity index 87% rename from app/src/main/java/space/taran/arkrate/ui/theme/Theme.kt rename to app/src/main/java/space/taran/arkrate/presentation/theme/Theme.kt index 74b1f3b1f..cbbf7799e 100644 --- a/app/src/main/java/space/taran/arkrate/ui/theme/Theme.kt +++ b/app/src/main/java/space/taran/arkrate/presentation/theme/Theme.kt @@ -1,4 +1,4 @@ -package space.taran.arkrate.ui.theme +package space.taran.arkrate.presentation.theme import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material.MaterialTheme @@ -28,7 +28,7 @@ private val LightColorPalette = lightColors( ) @Composable -fun ExchangeTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) { +fun ARKRateTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) { val colors = if (darkTheme) { DarkColorPalette } else { diff --git a/app/src/main/java/space/taran/arkrate/ui/theme/Type.kt b/app/src/main/java/space/taran/arkrate/presentation/theme/Type.kt similarity index 94% rename from app/src/main/java/space/taran/arkrate/ui/theme/Type.kt rename to app/src/main/java/space/taran/arkrate/presentation/theme/Type.kt index e4b1c7cb7..453cd2c07 100644 --- a/app/src/main/java/space/taran/arkrate/ui/theme/Type.kt +++ b/app/src/main/java/space/taran/arkrate/presentation/theme/Type.kt @@ -1,4 +1,4 @@ -package space.taran.arkrate.ui.theme +package space.taran.arkrate.presentation.theme import androidx.compose.material.Typography import androidx.compose.ui.text.TextStyle diff --git a/app/src/main/java/space/taran/arkrate/storage/Input.kt b/app/src/main/java/space/taran/arkrate/storage/Input.kt deleted file mode 100644 index d6f684746..000000000 --- a/app/src/main/java/space/taran/arkrate/storage/Input.kt +++ /dev/null @@ -1,74 +0,0 @@ -package space.taran.arkrate.storage - -import com.squareup.moshi.JsonClass -import com.squareup.moshi.Moshi -import java.io.File - -@JsonClass(generateAdapter = true) -data class total( - val total: MutableList -) - -@JsonClass(generateAdapter = true) -data class Exchange( - var name: String, - var number: Double -) - - -class AppDatabase(val filePath: String) { - - fun getAllExchange(): List { - if (!File(filePath).canWrite()) { - File(filePath).writeText("""{"total":[]}""") - } - val origin = File(filePath).readText() - val moshi: Moshi = Moshi.Builder().build() - val jsonAdapter = moshi.adapter(total::class.java) - val json = jsonAdapter.fromJson(origin) - return json?.total ?: listOf() - } - - fun setExchange(name: String, value: Double) { - - if (!File(filePath).canWrite()) { - File(filePath).writeText("""{"total":[]}""") - } - val origin = File(filePath).readText() - val moshi: Moshi = Moshi.Builder().build() - val jsonAdapter = moshi.adapter(total::class.java) - val json = jsonAdapter.fromJson(origin) ?: total(mutableListOf()) - for (i in (0 until if (json.total.size == 0) { - 1 - } else { - json.total.size - })) { - if (json.total.size > 0 && json.total[i].name == name) { - json.total[i].number = value - break - } - if (json.total.size == 0 || i == json.total.size - 1) { - json.total.add(Exchange(name, value)) - } - } - File(filePath).writeText(jsonAdapter.toJson(json)) - } - - fun remove(name: String) { - if (!File(filePath).canWrite()) { - File(filePath).writeText("""{"total":[]}""") - } - val origin = File(filePath).readText() - val moshi: Moshi = Moshi.Builder().build() - val jsonAdapter = moshi.adapter(total::class.java) - val json = jsonAdapter.fromJson(origin) ?: total(mutableListOf()) - - for (i in (0 until json.total.size)) { - if (json.total[i].name == name) { - json.total.removeAt(i) - break - } - } - File(filePath).writeText(jsonAdapter.toJson(json)) - } -} \ No newline at end of file diff --git a/app/src/main/java/space/taran/arkrate/ui/AddExchange.kt b/app/src/main/java/space/taran/arkrate/ui/AddExchange.kt deleted file mode 100644 index 7f2f4bb8a..000000000 --- a/app/src/main/java/space/taran/arkrate/ui/AddExchange.kt +++ /dev/null @@ -1,78 +0,0 @@ -package space.taran.arkrate.ui - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material.Divider -import androidx.compose.material.OutlinedTextField -import androidx.compose.material.Text -import androidx.compose.runtime.* -import androidx.compose.runtime.snapshots.SnapshotStateMap -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.unit.dp -import space.taran.arkrate.network.currencies -import space.taran.arkrate.storage.AppDatabase - -class AddExchange( - val appDatabase: AppDatabase, - val count: SnapshotStateMap, - val visible: SnapshotStateMap, -) { - @Composable - fun AddExchangeView( - modifier: Modifier, - searchResult: SnapshotStateMap, - filePath: String, - setNowActivity: () -> Unit, - ) { - var searchContent by remember { mutableStateOf("") } - val focusManager = LocalFocusManager.current - return Column(modifier = modifier) { - Row { - OutlinedTextField( - value = searchContent, - onValueChange = { it1 -> - searchContent = it1 - for (i in searchResult.keys.size - 1 downTo 0) { - searchResult.remove(searchResult.keys.toList()[i]) - } - val from = currencies.get(filePath = filePath).filter { - return@filter it.key.indexOf(it1.uppercase()) != -1 - } - searchResult.putAll(from) - }, - modifier = Modifier.fillMaxWidth(), - label = { - Text("Search") - } - ) - } - LazyColumn { - items(searchResult.keys.toList().sorted()) { - Box(Modifier.fillMaxWidth().height(32.dp).clickable(onClick = { - appDatabase.setExchange(it, -1.0) - count[it] = "-1.0" - visible[it] = true - focusManager.clearFocus() - setNowActivity() - })) { - Text( - it, - modifier = Modifier.align(Alignment.CenterStart), - ) - currencies.currencies[it]?.let { it1 -> - Text( - it1, - modifier = Modifier.align(Alignment.CenterEnd) - ) - } - } - Divider() - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/space/taran/arkrate/ui/InputActivity.kt b/app/src/main/java/space/taran/arkrate/ui/InputActivity.kt deleted file mode 100644 index 301532441..000000000 --- a/app/src/main/java/space/taran/arkrate/ui/InputActivity.kt +++ /dev/null @@ -1,111 +0,0 @@ -package space.taran.arkrate.ui - -import android.util.Log -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.slideOutHorizontally -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.text.KeyboardOptions -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.Clear -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.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshots.SnapshotStateMap -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.unit.dp -import space.taran.arkrate.storage.AppDatabase - -class InputActivity( - val appDatabase: AppDatabase, - val count: SnapshotStateMap, - val visible: SnapshotStateMap -) { - @Composable - fun InputView(modifier: Modifier) { - return LazyColumn(modifier = modifier) { - items(count.size) { i -> - AnimatedVisibility( - visible[count.keys.toList()[i]]!!, - exit = slideOutHorizontally() - ) { - Row(modifier.fillMaxWidth()) { - var textValue by remember { mutableStateOf(if ((count[count.keys.toList()[i]]?: "-1.0").toDouble() == -1.0 - ) "" else count[count.keys.toList()[i]].toString()) } - OutlinedTextField( - value = textValue, - onValueChange = { - textValue=it - var oneDot = true - val result = it.filter { - if (it == ".".toCharArray()[0] && oneDot) { - oneDot = false - Log.d("inputview", "InputView: $it") - return@filter true - } - return@filter it.isDigit() - } - appDatabase.setExchange( - count.keys.toList()[i], - if (result.isEmpty()) { - -1.0 - } else { - result.toDouble() - } - ) - count[count.keys.toList()[i]] = - (result.ifEmpty { (-1.0).toString() }) - }, - trailingIcon = { - Icon(Icons.Default.Clear, - contentDescription = "clear text", - modifier = Modifier - .clickable { - appDatabase.setExchange(count.keys.toList()[i], -1.0) - count[count.keys.toList()[i]] = "-1.0" - visible[count.keys.toList()[i]] = true - textValue="" - } - ) - }, - label = { Text(count.keys.toList()[i]) }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - modifier = Modifier.height(60.dp).fillMaxWidth(0.9f), - maxLines = 1 - ) - IconButton(onClick = { - appDatabase.remove(count.keys.toList()[i]) - visible[count.keys.toList()[i]] = false - }, modifier = Modifier.padding(8.dp)) { - Icon(Icons.Filled.Delete, "Delete") - } - } - } - } - - //if don't have any exchange show this text - if (count.size == 0) { - item { - Box(modifier = Modifier.fillMaxSize()) { - Text( - modifier = Modifier.align(Alignment.Center), - text = "Click the Add button at the top right to add a new exchange rate." - ) - } - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/space/taran/arkrate/ui/MainActivity.kt b/app/src/main/java/space/taran/arkrate/ui/MainActivity.kt deleted file mode 100644 index 785723e19..000000000 --- a/app/src/main/java/space/taran/arkrate/ui/MainActivity.kt +++ /dev/null @@ -1,220 +0,0 @@ -package space.taran.arkrate.ui - -import android.app.Activity -import android.os.Build -import androidx.activity.compose.BackHandler -import androidx.annotation.RequiresApi -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.fadeOut -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.material.ExtendedFloatingActionButton -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.Scaffold -import androidx.compose.material.Text -import androidx.compose.material.TopAppBar -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.ArrowBack -import androidx.compose.material.icons.filled.ArrowForward -import androidx.compose.material.icons.filled.Menu -import androidx.compose.material.rememberScaffoldState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateMapOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshots.SnapshotStateMap -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.unit.dp -import coil.ImageLoader -import coil.compose.rememberAsyncImagePainter -import coil.decode.GifDecoder -import coil.decode.ImageDecoderDecoder -import kotlin.concurrent.thread -import kotlinx.coroutines.launch -import space.taran.arkrate.R -import space.taran.arkrate.network.currencies -import space.taran.arkrate.storage.AppDatabase -import space.taran.arkrate.ui.theme.ExchangeTheme - -@RequiresApi(Build.VERSION_CODES.N) -@Composable -fun Create(filePath: String) { - val scope = rememberCoroutineScope() - val appDatabase = AppDatabase(filePath) - val scaffoldState = rememberScaffoldState() - val count = remember { mutableStateMapOf() } - val visibilities = remember { mutableStateMapOf() } - val viewVisibility = remember { - mutableStateMapOf( - Views.Input to true, - Views.Output to false, - Views.Add to false - ) - } - appDatabase.getAllExchange().forEach { - count[it.name] = it.number.toString() - visibilities[it.name] = true - } - var nowActivity by remember { - mutableStateOf(Views.Input) - } - val activity = (LocalContext.current as? Activity) - - BackHandler(true, onBack = { - if (nowActivity != Views.Input) { - viewVisibility[Views.Input] = true - viewVisibility[nowActivity] = false - nowActivity = Views.Input - } else - activity?.finish() - }) - return ExchangeTheme { - Scaffold( - scaffoldState = scaffoldState, - topBar = { - TopAppBar( - title = { Text(text = Views.Input.name) }, - navigationIcon = { - IconButton(onClick = { scope.launch { scaffoldState.drawerState.open() } }) { - Icon(Icons.Filled.Menu, contentDescription = null) - } - }, - actions = { - IconButton(onClick = { - viewVisibility[nowActivity] = false - viewVisibility[Views.Add] = true - nowActivity = Views.Add - - }) { - Icon(Icons.Filled.Add, contentDescription = "add transformation") - } - } - ) - }, - content = { contentPadding -> - Box(modifier = Modifier.padding(contentPadding)) { - AnimatedVisibility(viewVisibility[Views.Input]!!) { - InputActivity( - appDatabase, count, visibilities - ).InputView( - modifier = Modifier - .fillMaxSize() - .padding(start = 16.dp, top = 16.dp, end = 16.dp), - ) - } - AnimatedVisibility(viewVisibility[Views.Output]!!) { - OutputActivity().activity( - modifier = Modifier - .fillMaxSize() - .padding(start = 16.dp, top = 16.dp, end = 16.dp), count, filePath - ) - } - AnimatedVisibility(viewVisibility[Views.Add]!!) { - Box(Modifier.fillMaxSize()) { - var loading by remember { mutableStateOf(true) } - val a = remember> { mutableStateMapOf() } - thread { - a.putAll( - currencies.get(filePath) - ) - loading = false - } - AnimatedVisibility( - loading, - modifier = Modifier.align(Alignment.Center), - exit = fadeOut() - ) { - Column { - val imgLoader = ImageLoader.Builder(LocalContext.current) - .components { - if (Build.VERSION.SDK_INT >= 28) { - add(ImageDecoderDecoder.Factory()) - } else { - add(GifDecoder.Factory()) - } - } - .build() - val mPainter = rememberAsyncImagePainter( - R.drawable.loading, - imgLoader - ) - Image( - painter = mPainter, - "Loading...", - modifier = Modifier.size(64.dp) - ) - Text("Loading...") - } - } - AnimatedVisibility(!loading) { - AddExchange(appDatabase, count, visibilities).AddExchangeView( - Modifier - .fillMaxSize() - .padding(16.dp, 16.dp, 16.dp), - a, - filePath - ) { - viewVisibility[Views.Input] = true - viewVisibility[nowActivity] = false - nowActivity = Views.Input - } - } - } - } - } - }, - drawerContent = { - Column(modifier = Modifier.fillMaxSize()) { - Text(text = "not yet") - } - }, - floatingActionButton = { - ExtendedFloatingActionButton(text = { - when (nowActivity) { - Views.Input -> Icon(Icons.Filled.ArrowForward, "go to output") - Views.Output -> Icon(Icons.Filled.ArrowBack, "back to Input") - Views.Add -> Icon(Icons.Filled.ArrowBack, "back to Input") - } - }, onClick = { - visibilities.forEach { (s, b) -> - if (!b) { - count.remove(s) - } - } - when (nowActivity) { - Views.Add -> { - viewVisibility[Views.Input] = true - viewVisibility[nowActivity] = false - nowActivity = Views.Input - } - Views.Input -> { - viewVisibility[Views.Output] = true - viewVisibility[nowActivity] = false - nowActivity = Views.Output - } - Views.Output -> { - viewVisibility[Views.Input] = true - viewVisibility[nowActivity] = false - nowActivity = Views.Input - } - } - }) - } - ) - } -} - -enum class Views { - Input, Output, Add -} \ No newline at end of file diff --git a/app/src/main/java/space/taran/arkrate/ui/OutputActivity.kt b/app/src/main/java/space/taran/arkrate/ui/OutputActivity.kt deleted file mode 100644 index a1adc0539..000000000 --- a/app/src/main/java/space/taran/arkrate/ui/OutputActivity.kt +++ /dev/null @@ -1,224 +0,0 @@ -package space.taran.arkrate.ui - -import android.os.Build.VERSION.SDK_INT -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.fadeOut -import androidx.compose.foundation.Image -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material.Card -import androidx.compose.material.Divider -import androidx.compose.material.Text -import androidx.compose.runtime.* -import androidx.compose.runtime.snapshots.SnapshotStateMap -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import coil.ImageLoader -import coil.compose.rememberAsyncImagePainter -import coil.decode.GifDecoder -import coil.decode.ImageDecoderDecoder -import space.taran.arkrate.network.Crypto -import space.taran.arkrate.network.getAllRatesWithCache -import java.math.BigDecimal -import java.math.RoundingMode -import java.text.DecimalFormat -import kotlin.concurrent.thread - -class OutputActivity { - @Composable - fun activity( - modifier: Modifier, - count: SnapshotStateMap, - filePath: String, - ) { - var loading by remember { mutableStateOf(true) } - var rates: MutableMap - val total: SnapshotStateMap = remember { mutableStateMapOf() } - val exchange: SnapshotStateMap = remember { mutableStateMapOf() } - thread { - rates = getAllRatesWithCache(filePath).toMutableMap() - Crypto().getCryptoPriceWithCache(filePath).map { - if (it.key.takeLast(4) == "USDT") { - rates[it.key.dropLast(4)] = UtilsBigDecimal.div(1.0, it.value) - } - } - Thread.sleep(500) - calculateTotal(count, rates).forEach { - total[it.key] = it.value - } - calculateExchange(count, rates).forEach { - exchange[it.key] = it.value - } - loading = false - } - return Box(modifier = modifier) { - AnimatedVisibility( - loading, - modifier = Modifier.align(Alignment.Center), - exit = fadeOut() - ) { - Column { - val imgLoader = ImageLoader.Builder(LocalContext.current) - .components { - if (SDK_INT >= 28) { - add(ImageDecoderDecoder.Factory()) - } else { - add(GifDecoder.Factory()) - } - } - .build() - val mPainter = rememberAsyncImagePainter( - space.taran.arkrate.R.drawable.loading, - imgLoader - ) - Image(painter = mPainter, "Loading...", modifier = Modifier.size(64.dp)) - Text("Loading...") - } - } - AnimatedVisibility(!loading) { - LazyColumn { - item { - Card( - modifier = Modifier - .fillMaxWidth(), elevation = 8.dp - ) { - Column(modifier = Modifier.padding(8.dp)) { - Text("TOTAL", fontSize = 24.sp) - Divider() - total.forEach { - Box(modifier = Modifier.fillMaxWidth().height(32.dp)) { - val format = DecimalFormat("0.###") - format.roundingMode = RoundingMode.HALF_DOWN - Text( - it.key, - modifier = Modifier.align(Alignment.CenterStart) - ) - Text( - format.format(it.value), - modifier = Modifier.align(Alignment.CenterEnd) - ) - Divider() - } - } - } - } - } - item { - Spacer(Modifier.fillMaxWidth().height(16.dp)) - } - item { - Card( - modifier = Modifier.fillMaxWidth(), - elevation = 8.dp - ) { - Column(modifier = Modifier.padding(8.dp)) { - var visible by remember { mutableStateOf(true) } - Box(modifier = Modifier.clickable(onClick = { - visible = !visible - }).fillMaxWidth()) { - Text("EXCHANGE", fontSize = 24.sp) - } - Divider() - AnimatedVisibility(visible) { - Column { - exchange.forEach { - Box(modifier = Modifier.fillMaxWidth().height(32.dp)) { - val format = DecimalFormat("0.###") - format.roundingMode = RoundingMode.HALF_DOWN - Text( - it.key, - modifier = Modifier.align(Alignment.CenterStart) - ) - Text( - format.format(it.value), - modifier = Modifier.align(Alignment.CenterEnd) - ) - Divider() - } - } - } - } - } - } - } - item { - Spacer(Modifier.fillMaxWidth().height(64.dp)) - } - } - } - } - } - - private fun calculateTotal( - count: SnapshotStateMap, - rates: Map - ): Map { - val result = mutableMapOf() - var USD = 0.0 - - count.forEach { - USD += UtilsBigDecimal.mul( - if ((if (it.value.isEmpty()) 0.0 else it.value.toDouble()) == -1.0) 0.0 else it.value.toDouble(), - UtilsBigDecimal.div(1.0, rates[it.key]!!) - ) - } - - count.forEach { - result[it.key] = UtilsBigDecimal.mul(USD, rates[it.key]!!) - } - - return result - } - - private fun calculateExchange( - count: SnapshotStateMap, - rates: Map - ): Map { - - val result = mutableMapOf() - var USD = 0.0 - - count.forEach { - USD += UtilsBigDecimal.mul( - if (it.value.isEmpty()) 0.0 else it.value.toDouble(), - UtilsBigDecimal.div(1.0, rates[it.key]!!) - ) - } - - count.forEach { i -> - val iToUsd = UtilsBigDecimal.div(1.0, rates[i.key]!!) - count.forEach { j -> - if (j.key != i.key) { - result["${i.key}/${j.key}"] = - UtilsBigDecimal.mul(iToUsd, rates[j.key]!!) - } - } - } - - return result - } -} - -private object UtilsBigDecimal { - - // 需要精确至小数点后几位 - const val DECIMAL_POINT_NUMBER: Int = 8 - - // 加法运算 - @JvmStatic - fun mul(d1: Double, d2: Double): Double = - BigDecimal(d1).multiply(BigDecimal(d2)) - .setScale(DECIMAL_POINT_NUMBER, BigDecimal.ROUND_DOWN) - .toDouble() - - // 除法运算 - @JvmStatic - fun div(d1: Double, d2: Double): Double = - BigDecimal(d1).divide(BigDecimal(d2), DECIMAL_POINT_NUMBER, BigDecimal.ROUND_DOWN) - .toDouble() - -} \ No newline at end of file diff --git a/app/src/main/java/space/taran/arkrate/utils/CollectionsUtils.kt b/app/src/main/java/space/taran/arkrate/utils/CollectionsUtils.kt new file mode 100644 index 000000000..14f333c0b --- /dev/null +++ b/app/src/main/java/space/taran/arkrate/utils/CollectionsUtils.kt @@ -0,0 +1,9 @@ +package space.taran.arkrate.utils + +fun List.replace(targetItem: T, newItem: T) = map { + if (it == targetItem) { + newItem + } else { + it + } +} \ No newline at end of file diff --git a/app/src/main/java/space/taran/arkrate/utils/CoroutineUtils.kt b/app/src/main/java/space/taran/arkrate/utils/CoroutineUtils.kt new file mode 100644 index 000000000..80d506513 --- /dev/null +++ b/app/src/main/java/space/taran/arkrate/utils/CoroutineUtils.kt @@ -0,0 +1,17 @@ +package space.taran.arkrate.utils + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import kotlin.coroutines.CoroutineContext + +suspend fun withContextAndLock( + context: CoroutineContext, + mutex: Mutex, + block: suspend CoroutineScope.() -> T +): T = withContext(context) { + mutex.withLock { + block() + } +} \ 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 new file mode 100644 index 000000000..a82da5391 --- /dev/null +++ b/app/src/main/java/space/taran/arkrate/utils/FormatUtils.kt @@ -0,0 +1,10 @@ +package space.taran.arkrate.utils + +fun Double.removeFractionalPartIfEmpty(): String { + val integerPart = this.toInt() + val fractionalPart = integerPart - this + return if (fractionalPart == 0.0) + integerPart.toString() + else + this.toString() +} \ No newline at end of file diff --git a/app/src/main/res/drawable/loading.gif b/app/src/main/res/drawable/loading.gif deleted file mode 100644 index cbaf6414a1cab5f6e12459291fb2b4f502360640..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 61465 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 S8~Eo2{<(pFZs5Of;Qs?wfKYk> diff --git a/build.gradle b/build.gradle index 8c50d0525..54694c7f1 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ buildscript { ext { - compose_version = '1.3.0-beta01' + compose_version = '1.3.0' } ext.kotlin_version="1.7.10" }// Top-level build file where you can add configuration options common to all sub-projects/modules.