Skip to content

Commit

Permalink
WIP: ExchangeRatesScreen.kt + fix bugs
Browse files Browse the repository at this point in the history
  • Loading branch information
ILIYANGERMANOV committed Dec 27, 2022
1 parent c9ef7b3 commit be86fac
Show file tree
Hide file tree
Showing 12 changed files with 676 additions and 74 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,17 @@ class SyncExchangeRatesAct @Inject constructor(
}
}.toList()
Timber.d("Updating exchange rates: $rateEntities")
exchangeRateDao.saveAll(rateEntities)
rateEntities.forEach { newRate ->
val manualOverride = exchangeRateDao.findByBaseCurrencyAndCurrency(
baseCurrency = newRate.baseCurrency,
currency = newRate.currency
)?.manualOverride ?: false

if (!manualOverride && newRate.rate > 0.0) {
// save only the once that aren't overridden
exchangeRateDao.save(newRate)
}
}
}

private suspend fun fetchEurRates(url: String): Map<String, Double> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.ivy.wallet.io.persistence.data.ExchangeRateEntity
import kotlinx.coroutines.flow.Flow

@Dao
interface ExchangeRateDao {
Expand All @@ -14,12 +15,21 @@ interface ExchangeRateDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun saveAll(value: List<ExchangeRateEntity>)

@Query("SELECT * FROM exchange_rates")
fun findAll(): Flow<List<ExchangeRateEntity>>

@Query("SELECT * FROM exchange_rates WHERE baseCurrency = :baseCurrency AND currency = :currency")
suspend fun findByBaseCurrencyAndCurrency(
baseCurrency: String,
currency: String
): ExchangeRateEntity?

@Query("DELETE FROM exchange_rates WHERE baseCurrency = :baseCurrency AND currency = :currency")
suspend fun deleteByBaseCurrencyAndCurrency(
baseCurrency: String,
currency: String
)

@Query("DELETE FROM exchange_rates")
suspend fun deleteALl()
}
2 changes: 2 additions & 0 deletions app/src/main/java/com/ivy/wallet/ui/RootActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import com.ivy.wallet.ui.charts.ChartsScreen
import com.ivy.wallet.ui.csvimport.ImportCSVScreen
import com.ivy.wallet.ui.donate.DonateScreen
import com.ivy.wallet.ui.edit.EditTransactionScreen
import com.ivy.wallet.ui.exchangerates.ExchangeRatesScreen
import com.ivy.wallet.ui.experiment.images.ImagesScreen
import com.ivy.wallet.ui.loan.LoansScreen
import com.ivy.wallet.ui.loandetails.LoanDetailsScreen
Expand Down Expand Up @@ -191,6 +192,7 @@ class RootActivity : AppCompatActivity() {
is Main -> MainScreen(screen = screen)
is Onboarding -> OnboardingScreen(screen = screen)
is ServerStop -> ServerStopScreen()
is ExchangeRatesScreen -> ExchangeRatesScreen()
is EditTransaction -> EditTransactionScreen(screen = screen)
is ItemStatistic -> ItemStatisticScreen(screen = screen)
is PieChartStatistic -> PieChartStatisticScreen(screen = screen)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
package com.ivy.wallet.ui.exchangerates

import androidx.compose.foundation.layout.BoxWithConstraintsScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.items
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.accompanist.insets.systemBarsPadding
import com.ivy.design.l0_system.UI
import com.ivy.design.l0_system.White
import com.ivy.design.l0_system.style
import com.ivy.design.l1_buildingBlocks.ColumnRoot
import com.ivy.design.l1_buildingBlocks.DividerW
import com.ivy.design.l1_buildingBlocks.SpacerHor
import com.ivy.design.l1_buildingBlocks.SpacerVer
import com.ivy.frp.view.navigation.Screen
import com.ivy.wallet.ui.IvyWalletPreview
import com.ivy.wallet.ui.exchangerates.component.RateItem
import com.ivy.wallet.ui.exchangerates.data.RateUi
import com.ivy.wallet.ui.exchangerates.modal.AddRateModal
import com.ivy.wallet.ui.search.SearchInput
import com.ivy.wallet.ui.theme.modal.edit.AmountModal
import com.ivy.wallet.utils.selectEndTextFieldValue
import java.util.*

object ExchangeRatesScreen : Screen

@Composable
fun BoxWithConstraintsScope.ExchangeRatesScreen() {
val viewModel: ExchangeRatesViewModel = viewModel()
val state by viewModel.state.collectAsState()

UI(
state = state,
onEvent = viewModel::onEvent,
)
}

@Composable
private fun BoxWithConstraintsScope.UI(
state: RatesState,
onEvent: (RatesEvent) -> Unit,
) {
var amountModalVisible by remember {
mutableStateOf(false)
}
var rateToUpdate by remember {
mutableStateOf<RateUi?>(null)
}

val onRateClick = { rate: RateUi ->
rateToUpdate = rate
amountModalVisible = true
}

ColumnRoot {
SpacerVer(height = 16.dp)
SearchField(onSearch = { onEvent(RatesEvent.Search(it)) })
SpacerVer(height = 4.dp)
LazyColumn {
ratesSection(text = "Manual")
items(items = state.manual) { rate ->
SpacerVer(height = 4.dp)
RateItem(
rate = rate,
onDelete = { onEvent(RatesEvent.RemoveOverride(rate)) },
onClick = { onRateClick(rate) }
)
}
ratesSection(text = "Automatic")
items(items = state.automatic) { rate ->
SpacerVer(height = 4.dp)
RateItem(
rate = rate,
onDelete = null,
onClick = { onRateClick(rate) }
)
}
item(key = "last_item_spacer") {
SpacerVer(height = 48.dp)
}
}
}

var addRateModalVisible by remember {
mutableStateOf(false)
}
Button(
modifier = Modifier
.systemBarsPadding()
.align(Alignment.BottomCenter)
.padding(bottom = 24.dp),
colors = ButtonDefaults.buttonColors(
backgroundColor = UI.colors.primary
),
onClick = {
addRateModalVisible = true
}
) {
Text(
text = "Add rate",
style = UI.typo.b1.style(
color = White
)
)
}
AddRateModal(
visible = addRateModalVisible,
baseCurrency = state.baseCurrency,
dismiss = {
addRateModalVisible = false
},
onAdd = onEvent
)

AmountModal(
id = remember { UUID.randomUUID() },
visible = amountModalVisible,
currency = "",
initialAmount = rateToUpdate?.rate,
dismiss = {
amountModalVisible = false
},
decimalCountMax = 12,
onAmountChanged = { newRate ->
rateToUpdate?.let {
onEvent(RatesEvent.UpdateRate(rateToUpdate!!, newRate))
}
}
)
}

private fun LazyListScope.ratesSection(
text: String
) {
item {
SpacerVer(height = 24.dp)
Row(verticalAlignment = Alignment.CenterVertically) {
DividerW()
SpacerHor(width = 16.dp)
Text(
text = text,
style = UI.typo.h2
)
SpacerHor(width = 16.dp)
DividerW()
}
}
}

@Composable
private fun SearchField(
onSearch: (String) -> Unit,
) {
var searchQueryTextFieldValue by remember {
mutableStateOf(selectEndTextFieldValue(""))
}

SearchInput(
searchQueryTextFieldValue = searchQueryTextFieldValue,
hint = "Search currency",
onSetSearchQueryTextField = {
searchQueryTextFieldValue = it
onSearch(it.text)
}
)
}


@Preview
@Composable
private fun Preview() {
IvyWalletPreview {
UI(
state = RatesState(
baseCurrency = "BGN",
manual = listOf(
RateUi("BGN", "USD", 1.85),
RateUi("BGN", "EUR", 1.96),
),
automatic = listOf(
RateUi("XXX", "YYY", 1.23),
RateUi("XXX", "YYY", 1.23),
RateUi("XXX", "YYY", 1.23),
RateUi("XXX", "YYY", 1.23),
RateUi("XXX", "YYY", 1.23),
RateUi("XXX", "YYY", 1.23),
RateUi("XXX", "YYY", 1.23),
RateUi("XXX", "YYY", 1.23),
RateUi("XXX", "YYY", 1.23),
RateUi("XXX", "YYY", 1.23),
RateUi("XXX", "YYY", 1.23),
RateUi("XXX", "YYY", 1.23),
)
),
onEvent = {}
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package com.ivy.wallet.ui.exchangerates

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.ivy.wallet.domain.action.exchange.SyncExchangeRatesAct
import com.ivy.wallet.domain.action.settings.BaseCurrencyAct
import com.ivy.wallet.io.persistence.dao.ExchangeRateDao
import com.ivy.wallet.io.persistence.data.ExchangeRateEntity
import com.ivy.wallet.ui.exchangerates.data.RateUi
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject

@HiltViewModel
class ExchangeRatesViewModel @Inject constructor(
private val exchangeRateDao: ExchangeRateDao,
private val baseCurrencyAct: BaseCurrencyAct,
private val syncExchangeRatesAct: SyncExchangeRatesAct
) : ViewModel() {
private val searchQuery = MutableStateFlow("")

val state = combine(
exchangeRateDao.findAll(),
searchQuery
) { rates, query ->
if (query.isNotBlank()) {
rates.filter {
it.currency.contains(query, true)
}
} else {
rates
}
}.map { rates ->
RatesState(
baseCurrency = baseCurrencyAct(Unit),
manual = rates.filter { it.manualOverride }.map(::toUi),
automatic = rates.filter { !it.manualOverride }.map(::toUi)
)
}.stateIn(
viewModelScope,
SharingStarted.Eagerly,
RatesState(
baseCurrency = "",
manual = emptyList(),
automatic = emptyList()
)
)

private fun toUi(entity: ExchangeRateEntity) = RateUi(
from = entity.baseCurrency,
to = entity.currency,
rate = entity.rate
)


// region Event Handling
fun onEvent(event: RatesEvent) {
viewModelScope.launch {
when (event) {
is RatesEvent.RemoveOverride -> handleRemoveOverride(event)
is RatesEvent.Search -> handleSearch(event)
is RatesEvent.UpdateRate -> handleUpdateRate(event)
is RatesEvent.AddRate -> handleAddRate(event)
}
}
}

private suspend fun handleRemoveOverride(event: RatesEvent.RemoveOverride) {
withContext(Dispatchers.IO) {
exchangeRateDao.deleteByBaseCurrencyAndCurrency(
baseCurrency = event.rate.from,
currency = event.rate.to
)
}
sync()
}

private fun handleSearch(event: RatesEvent.Search) {
searchQuery.value = event.query.trim()
}

private suspend fun handleUpdateRate(event: RatesEvent.UpdateRate) {
withContext(Dispatchers.IO) {
if (event.newRate > 0.0) {
exchangeRateDao.save(
ExchangeRateEntity(
baseCurrency = event.rate.from,
currency = event.rate.to,
rate = event.newRate,
manualOverride = true
)
)
}
}
}

private suspend fun handleAddRate(event: RatesEvent.AddRate) {
withContext(Dispatchers.IO) {
if (event.rate.rate > 0.0) {
exchangeRateDao.save(
ExchangeRateEntity(
baseCurrency = event.rate.from.uppercase().trim(),
currency = event.rate.to.uppercase().trim(),
rate = event.rate.rate,
manualOverride = true
)
)
}
}
}

private suspend fun sync() {
syncExchangeRatesAct(SyncExchangeRatesAct.Input(baseCurrencyAct(Unit)))
}
// endregion
}
Loading

0 comments on commit be86fac

Please sign in to comment.