mirror of
https://github.com/Myzel394/NumberHub.git
synced 2025-06-20 01:05:26 +02:00
432 lines
16 KiB
Kotlin
432 lines
16 KiB
Kotlin
package com.sadellie.unitto.screens
|
|
|
|
import android.app.Application
|
|
import android.util.Log
|
|
import androidx.compose.runtime.getValue
|
|
import androidx.compose.runtime.mutableStateOf
|
|
import androidx.compose.runtime.setValue
|
|
import androidx.lifecycle.ViewModel
|
|
import androidx.lifecycle.viewModelScope
|
|
import com.google.firebase.analytics.FirebaseAnalytics
|
|
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
|
import com.sadellie.unitto.data.KEY_0
|
|
import com.sadellie.unitto.data.KEY_DOT
|
|
import com.sadellie.unitto.data.KEY_MINUS
|
|
import com.sadellie.unitto.data.preferences.AppTheme
|
|
import com.sadellie.unitto.data.preferences.MAX_PRECISION
|
|
import com.sadellie.unitto.data.preferences.OutputFormat
|
|
import com.sadellie.unitto.data.preferences.Separator
|
|
import com.sadellie.unitto.data.preferences.UserPreferencesRepository
|
|
import com.sadellie.unitto.data.units.AbstractUnit
|
|
import com.sadellie.unitto.data.units.AllUnitsRepository
|
|
import com.sadellie.unitto.data.units.MyUnitIDS
|
|
import com.sadellie.unitto.data.units.UnitGroup
|
|
import com.sadellie.unitto.data.units.database.MyBasedUnit
|
|
import com.sadellie.unitto.data.units.database.MyBasedUnitsRepository
|
|
import com.sadellie.unitto.data.units.remote.CurrencyApi
|
|
import com.sadellie.unitto.data.units.remote.CurrencyUnitResponse
|
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
import kotlinx.coroutines.flow.first
|
|
import kotlinx.coroutines.launch
|
|
import java.math.BigDecimal
|
|
import javax.inject.Inject
|
|
|
|
|
|
@HiltViewModel
|
|
class MainViewModel @Inject constructor(
|
|
private val userPrefsRepository: UserPreferencesRepository,
|
|
private val basedUnitRepository: MyBasedUnitsRepository,
|
|
private val application: Application,
|
|
private val allUnitsRepository: AllUnitsRepository
|
|
) : ViewModel() {
|
|
|
|
var currentTheme: Int by mutableStateOf(AppTheme.NOT_SET)
|
|
private set
|
|
var precision: Int by mutableStateOf(3)
|
|
private set
|
|
var separator: Int by mutableStateOf(Separator.SPACES)
|
|
private set
|
|
var outputFormat: Int by mutableStateOf(OutputFormat.PLAIN)
|
|
private set
|
|
var enableAnalytics: Boolean by mutableStateOf(false)
|
|
private set
|
|
|
|
/**
|
|
* See [UserPreferencesRepository.updateCurrentAppTheme]
|
|
*/
|
|
fun updateCurrentAppTheme(appTheme: Int) {
|
|
viewModelScope.launch {
|
|
userPrefsRepository.updateCurrentAppTheme(appTheme)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* See [UserPreferencesRepository.updateDigitsPrecision]
|
|
*/
|
|
fun updatePrecision(precision: Int) {
|
|
viewModelScope.launch {
|
|
userPrefsRepository.updateDigitsPrecision(precision)
|
|
convertValue()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* See [UserPreferencesRepository.updateSeparator]
|
|
*/
|
|
fun updateSeparator(separator: Int) {
|
|
viewModelScope.launch {
|
|
userPrefsRepository.updateSeparator(separator)
|
|
convertValue()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* See [UserPreferencesRepository.updateOutputFormat]
|
|
*/
|
|
fun updateOutputFormat(outputFormat: Int) {
|
|
viewModelScope.launch {
|
|
userPrefsRepository.updateOutputFormat(outputFormat)
|
|
convertValue()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* See [UserPreferencesRepository.updateEnableAnalytics]
|
|
*/
|
|
fun updateEnableAnalytics(enableAnalytics: Boolean) {
|
|
viewModelScope.launch {
|
|
userPrefsRepository.updateEnableAnalytics(enableAnalytics)
|
|
FirebaseAnalytics.getInstance(application).setAnalyticsCollectionEnabled(enableAnalytics)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Unit we converting from (left side)
|
|
*/
|
|
var unitFrom: AbstractUnit by mutableStateOf(allUnitsRepository.getById(MyUnitIDS.kilometer))
|
|
private set
|
|
|
|
/**
|
|
* Unit we are converting to (right side)
|
|
*/
|
|
var unitTo: AbstractUnit by mutableStateOf(allUnitsRepository.getById(MyUnitIDS.mile))
|
|
private set
|
|
|
|
/**
|
|
* UI state
|
|
*/
|
|
var mainUIState: MainScreenUIState by mutableStateOf(MainScreenUIState())
|
|
private set
|
|
|
|
/**
|
|
* This function takes local variables, converts values and then causes the UI to update
|
|
*/
|
|
private fun convertValue() {
|
|
// Converting value using a specified precision
|
|
val convertedValue: BigDecimal =
|
|
unitFrom.convert(unitTo, mainUIState.inputValue.toBigDecimal(), precision)
|
|
// Setting result value using a specified OutputFormat
|
|
mainUIState = mainUIState.copy(
|
|
resultValue = when (outputFormat) {
|
|
OutputFormat.ALLOW_ENGINEERING -> convertedValue.toString()
|
|
OutputFormat.FORCE_ENGINEERING -> convertedValue.toEngineeringString()
|
|
else -> convertedValue.toPlainString()
|
|
}
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Change left side unit. Unit to convert from
|
|
*
|
|
* @param clickedUnit Unit we need to change to
|
|
*/
|
|
fun changeUnitFrom(clickedUnit: AbstractUnit) {
|
|
// First we change unit
|
|
unitFrom = clickedUnit
|
|
|
|
// Now we check for negate button
|
|
mainUIState = mainUIState.copy(negateButtonEnabled = clickedUnit.group.canNegate)
|
|
// Now we change to positive if the group we switched to supports negate
|
|
if (!clickedUnit.group.canNegate) {
|
|
mainUIState =
|
|
mainUIState.copy(inputValue = mainUIState.inputValue.removePrefix(KEY_MINUS))
|
|
}
|
|
|
|
// Now setting up right unit (pair for the left one)
|
|
unitTo = if (unitFrom.pairedUnit == null) {
|
|
// Dangerous btw
|
|
allUnitsRepository.getCollectionByGroup(unitFrom.group)!!.first()
|
|
} else {
|
|
allUnitsRepository.getById(unitFrom.pairedUnit!!)
|
|
}
|
|
|
|
viewModelScope.launch {
|
|
// We need to increment counter for the clicked unit
|
|
incrementCounter(clickedUnit)
|
|
// Currencies require us to get data from the internet
|
|
updateCurrenciesBasicUnits()
|
|
// We can't call outside of this block. It will set precision to 0 in that case
|
|
convertValue()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Change right side unit. Unit to convert to
|
|
*
|
|
* @param clickedUnit Unit we need to change to
|
|
*/
|
|
fun changeUnitTo(clickedUnit: AbstractUnit) {
|
|
// First we change unit
|
|
unitTo = clickedUnit
|
|
// Updating paired unit for left side unit in memory (same thing for database below)
|
|
unitFrom.pairedUnit = unitTo.unitId
|
|
|
|
viewModelScope.launch {
|
|
// Updating paired unit for left side unit in database
|
|
basedUnitRepository.insertUnits(
|
|
MyBasedUnit(
|
|
unitId = unitFrom.unitId,
|
|
isFavorite = unitFrom.isFavorite,
|
|
pairedUnitId = unitFrom.pairedUnit,
|
|
frequency = unitFrom.counter
|
|
)
|
|
)
|
|
// We also need to increment counter for the selected unit
|
|
incrementCounter(clickedUnit)
|
|
}
|
|
|
|
// Changed units, now we can convert
|
|
convertValue()
|
|
}
|
|
|
|
private suspend fun incrementCounter(unit: AbstractUnit) {
|
|
basedUnitRepository.insertUnits(
|
|
MyBasedUnit(
|
|
unitId = unit.unitId,
|
|
isFavorite = unit.isFavorite,
|
|
pairedUnitId = unit.pairedUnit,
|
|
// This will increment counter on unit in list too
|
|
frequency = ++unit.counter
|
|
)
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Updates basic units properties for all currencies, BUT only when [unitFrom]'s group is set
|
|
* to [UnitGroup.CURRENCY].
|
|
*/
|
|
private suspend fun updateCurrenciesBasicUnits() {
|
|
// Resetting error and network loading states in case we are not gonna do anything below
|
|
mainUIState = mainUIState.copy(isLoadingNetwork = false, showError = false)
|
|
// We update currencies only when needed
|
|
if (unitFrom.group != UnitGroup.CURRENCY) return
|
|
|
|
// Starting to load stuff
|
|
mainUIState = mainUIState.copy(isLoadingNetwork = true)
|
|
|
|
try {
|
|
val pairs: CurrencyUnitResponse =
|
|
CurrencyApi.retrofitService.getCurrencyPairs(unitFrom.unitId)
|
|
allUnitsRepository.getCollectionByGroup(UnitGroup.CURRENCY)?.forEach {
|
|
// Getting rates from map. We set ZERO as default so that it can be skipped
|
|
val rate = pairs.currency.getOrElse(it.unitId) { BigDecimal.ZERO }
|
|
// We make sure that we don't divide by zero
|
|
if (rate > BigDecimal.ZERO) {
|
|
it.isEnabled = true
|
|
it.basicUnit = BigDecimal.ONE.setScale(MAX_PRECISION).div(rate)
|
|
} else {
|
|
// Hiding broken currencies
|
|
it.isEnabled = false
|
|
}
|
|
}
|
|
} catch (e: Exception) {
|
|
when (e) {
|
|
// 403, Network and Adapter exceptions can be ignored
|
|
is retrofit2.HttpException, is java.net.UnknownHostException, is com.squareup.moshi.JsonDataException -> {}
|
|
else -> {
|
|
// Unexpected exception, should report it
|
|
FirebaseCrashlytics.getInstance().recordException(e)
|
|
}
|
|
}
|
|
mainUIState = mainUIState.copy(showError = true)
|
|
} finally {
|
|
// Loaded
|
|
mainUIState = mainUIState.copy(isLoadingNetwork = false)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Swaps measurement, left to right and vice versa
|
|
*/
|
|
fun swapUnits() {
|
|
unitFrom = unitTo.also {
|
|
unitTo = unitFrom
|
|
}
|
|
viewModelScope.launch { updateCurrenciesBasicUnits() }
|
|
// Swapped, can convert now
|
|
convertValue()
|
|
}
|
|
|
|
/**
|
|
* Function to process input when we click keyboard. Make sure that digits/symbols will be
|
|
* added properly
|
|
* @param[digitToAdd] Digit/Symbol we want to add, can be any digit 0..9 or a dot symbol
|
|
*/
|
|
fun processInput(digitToAdd: String) {
|
|
when (digitToAdd) {
|
|
KEY_DOT -> {
|
|
// Here we add a dot to input
|
|
// Disabling dot button to avoid multiple dots in input value
|
|
// Enabling delete button to so that we can delete this dot from input
|
|
mainUIState = mainUIState.copy(
|
|
inputValue = mainUIState.inputValue + digitToAdd,
|
|
dotButtonEnabled = false,
|
|
deleteButtonEnabled = true
|
|
)
|
|
}
|
|
KEY_0 -> {
|
|
// We shouldn't add zero to another zero in input, i.e. 00
|
|
if (mainUIState.inputValue != KEY_0) {
|
|
mainUIState = mainUIState.copy(inputValue = mainUIState.inputValue + digitToAdd)
|
|
}
|
|
}
|
|
else -> {
|
|
/*
|
|
We want to add digit to input.
|
|
When there is just a zero, we should replace it with the digit we want to add,
|
|
avoids input to be like 03 (with this check it will be just 3)
|
|
*/
|
|
mainUIState = mainUIState.copy(
|
|
inputValue = if (mainUIState.inputValue == KEY_0) digitToAdd else mainUIState.inputValue + digitToAdd,
|
|
deleteButtonEnabled = true
|
|
)
|
|
}
|
|
}
|
|
convertValue()
|
|
}
|
|
|
|
/**
|
|
* Deletes last symbol from input and handles buttons state (enabled/disabled)
|
|
*/
|
|
fun deleteDigit() {
|
|
// Last symbol is a dot
|
|
// We enable DOT button
|
|
if (mainUIState.inputValue.endsWith(KEY_DOT)) {
|
|
mainUIState = mainUIState.copy(dotButtonEnabled = true)
|
|
}
|
|
|
|
// Deleting last symbol
|
|
mainUIState = mainUIState.copy(inputValue = mainUIState.inputValue.dropLast(1))
|
|
|
|
/*
|
|
Now we check what we have left
|
|
We deleted last symbol and we got Empty string, just minus symbol, or zero
|
|
Do not allow deleting anything beyond this (disable button)
|
|
Set input to default (zero)
|
|
Skipping this block means that we are left we acceptable value, i.e. 123.03
|
|
*/
|
|
if (
|
|
mainUIState.inputValue in listOf(String(), KEY_MINUS, KEY_0)
|
|
) {
|
|
mainUIState = mainUIState.copy(deleteButtonEnabled = false, inputValue = KEY_0)
|
|
}
|
|
|
|
// We are sure that input has acceptable value, so we convert it
|
|
convertValue()
|
|
}
|
|
|
|
/**
|
|
* Clears input value and sets it to default (ZERO)
|
|
*/
|
|
fun clearInput() {
|
|
mainUIState = mainUIState.copy(
|
|
inputValue = KEY_0,
|
|
deleteButtonEnabled = false,
|
|
dotButtonEnabled = true
|
|
)
|
|
convertValue()
|
|
}
|
|
|
|
/**
|
|
* Changes input from positive to negative and vice versa
|
|
*/
|
|
fun negateInput() {
|
|
mainUIState = mainUIState.copy(
|
|
inputValue = if (mainUIState.inputValue.getOrNull(0) != KEY_MINUS.single()) {
|
|
// If input doesn't have minus at the beginning, we give it to it
|
|
KEY_MINUS + mainUIState.inputValue
|
|
} else {
|
|
// Input has minus, meaning we need to remove it
|
|
mainUIState.inputValue.removePrefix(KEY_MINUS)
|
|
}
|
|
)
|
|
convertValue()
|
|
}
|
|
|
|
/**
|
|
* Saves latest pair of units into datastore
|
|
*/
|
|
fun saveLatestPairOfUnits() {
|
|
viewModelScope.launch {
|
|
userPrefsRepository.updateLatestPairOfUnits(unitFrom, unitTo)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Observes changes in user preferences and updated values in this ViewModel.
|
|
*
|
|
* Any change in user preferences will update mutableStateOf so composables, that rely on this
|
|
* values recompose when actually needed. For example, when you change output format, composable
|
|
* in MainActivity will not be recomposed even though it needs currentTheme which is also in
|
|
* user preferences/
|
|
*/
|
|
private fun observePreferenceChanges() {
|
|
viewModelScope.launch {
|
|
userPrefsRepository.userPreferencesFlow.collect { userPref ->
|
|
currentTheme = userPref.currentAppTheme
|
|
precision = userPref.digitsPrecision
|
|
separator = userPref.separator.also { Formatter.setSeparator(it) }
|
|
outputFormat = userPref.outputFormat
|
|
enableAnalytics = userPref.enableAnalytics.also {
|
|
// Maybe this is unnecessary
|
|
if (it != enableAnalytics) FirebaseAnalytics.getInstance(application)
|
|
.setAnalyticsCollectionEnabled(enableAnalytics)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
init {
|
|
observePreferenceChanges()
|
|
viewModelScope.launch {
|
|
val snapshot = userPrefsRepository.userPreferencesFlow.first()
|
|
|
|
// First we load latest pair of units
|
|
unitFrom = try {
|
|
allUnitsRepository.getById(snapshot.latestLeftSideUnit)
|
|
} catch (e: java.util.NoSuchElementException) {
|
|
Log.w("MainViewModel", "No unit with the given unitId")
|
|
allUnitsRepository.getById(MyUnitIDS.kilometer)
|
|
}
|
|
|
|
unitTo = try {
|
|
allUnitsRepository.getById(snapshot.latestRightSideUnit)
|
|
} catch (e: java.util.NoSuchElementException) {
|
|
Log.w("MainViewModel", "No unit with the given unitId")
|
|
allUnitsRepository.getById(MyUnitIDS.mile)
|
|
}
|
|
|
|
mainUIState = mainUIState.copy(negateButtonEnabled = unitFrom.group.canNegate)
|
|
|
|
// Now we load units data from database
|
|
val allBasedUnits = basedUnitRepository.getAll()
|
|
allUnitsRepository.loadFromDatabase(application, allBasedUnits)
|
|
|
|
// User is free to convert values and units on units screen can be sorted properly
|
|
mainUIState = mainUIState.copy(isLoadingDatabase = false)
|
|
updateCurrenciesBasicUnits()
|
|
convertValue()
|
|
}
|
|
}
|
|
}
|