Show additional info for unit and currency converter (#720)

* Now for conversion patterns such as "TO > FROM" and "TO - FROM" are accepted

Signed-off-by: Guillermo Villafuerte <gvillafu@comunidad.unam.mx>

* Added help URL for unit converter

Signed-off-by: Guillermo Villafuerte <gvillafu@comunidad.unam.mx>

* Localized supported units are now shown on a screen that can be invoked from unit converter settings

Signed-off-by: Guillermo Villafuerte <gvillafu@comunidad.unam.mx>

* Made localizable titles for dimensions and preferences

Signed-off-by: Guillermo Villafuerte <gvillafu@comunidad.unam.mx>

* Show currency units on list if enabled.

Signed-off-by: Guillermo Villafuerte <gvillafu@comunidad.unam.mx>

* Supported units are now listed inline instead of a separate screen.

Signed-off-by: Guillermo Villafuerte <gvillafu@comunidad.unam.mx>

---------

Signed-off-by: Guillermo Villafuerte <gvillafu@comunidad.unam.mx>
This commit is contained in:
Guillermo Villafuerte 2024-08-12 07:13:02 -06:00 committed by GitHub
parent 1a4f4d5e96
commit 827eb08908
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 169 additions and 31 deletions

View File

@ -3,11 +3,13 @@ package de.mm20.launcher2.ui.launcher.search.unitconverter
import android.icu.text.DateFormat import android.icu.text.DateFormat
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.* import androidx.compose.material.icons.rounded.*
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
@ -15,6 +17,7 @@ import androidx.compose.ui.unit.dp
import de.mm20.launcher2.search.data.CurrencyUnitConverter import de.mm20.launcher2.search.data.CurrencyUnitConverter
import de.mm20.launcher2.search.data.UnitConverter import de.mm20.launcher2.search.data.UnitConverter
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import de.mm20.launcher2.unitconverter.Dimension
import java.util.* import java.util.*
@Composable @Composable

View File

@ -1,19 +1,49 @@
package de.mm20.launcher2.ui.settings.unitconverter package de.mm20.launcher2.ui.settings.unitconverter
import android.icu.util.Currency
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.preferences.search.UnitConverterSettings
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.preferences.Preference
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
import de.mm20.launcher2.ui.component.preferences.SwitchPreference import de.mm20.launcher2.ui.component.preferences.SwitchPreference
import de.mm20.launcher2.ui.launcher.search.unitconverter.getDimensionIcon
import de.mm20.launcher2.unitconverter.converters.CurrencyConverter
import de.mm20.launcher2.unitconverter.converters.SimpleFactorConverter
import de.mm20.launcher2.unitconverter.converters.TemperatureConverter
import org.koin.androidx.compose.inject
@Composable @Composable
fun UnitConverterSettingsScreen() { fun UnitConverterSettingsScreen() {
val settings: UnitConverterSettings by inject()
val viewModel: UnitConverterSettingsScreenVM = viewModel() val viewModel: UnitConverterSettingsScreenVM = viewModel()
PreferenceScreen(title = stringResource(R.string.preference_search_unitconverter)) { val loading by viewModel.loading
val currenciesEnabled by settings.currencies.collectAsState(initial = false)
LaunchedEffect(currenciesEnabled) {
viewModel.loadCurrencies()
}
PreferenceScreen(title = stringResource(R.string.preference_search_unitconverter),
helpUrl = "https://kvaesitso.mm20.de/docs/user-guide/search/unit-converter") {
if (loading) {
item {
LinearProgressIndicator(
modifier = Modifier.fillMaxWidth()
)
}
}
item { item {
PreferenceCategory { PreferenceCategory {
val unitConverter by viewModel.unitConverter.collectAsState() val unitConverter by viewModel.unitConverter.collectAsState()
@ -36,6 +66,46 @@ fun UnitConverterSettingsScreen() {
} }
) )
} }
PreferenceCategory(
title = stringResource(R.string.preference_search_supportedunits)
) {
for (converter in viewModel.convertersList.value) {
val units = buildString {
when (converter) {
is SimpleFactorConverter -> {
converter.standardUnits.forEachIndexed { index, unit ->
if (index > 0) append(", ")
append(pluralStringResource(unit.nameResource, 1))
append(" (${unit.symbol})")
}
}
is TemperatureConverter -> {
converter.units.forEachIndexed { index, unit ->
if (index > 0) append(", ")
append(pluralStringResource(unit.nameResource, 1))
append(" (${unit.symbol})")
}
}
is CurrencyConverter -> {
viewModel.currenciesList.value.forEachIndexed { index, currency ->
if (index > 0) append(", ")
append(Currency.getInstance(currency)?.displayName ?: currency)
append(" ($currency)")
}
}
}
}
Preference(
title = stringResource(converter.dimension.resource),
icon = getDimensionIcon(converter.dimension),
summary = units
)
}
}
} }
} }
} }

View File

@ -1,9 +1,16 @@
package de.mm20.launcher2.ui.settings.unitconverter package de.mm20.launcher2.ui.settings.unitconverter
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.preferences.search.UnitConverterSettings import de.mm20.launcher2.preferences.search.UnitConverterSettings
import de.mm20.launcher2.unitconverter.UnitConverterRepository
import de.mm20.launcher2.unitconverter.converters.Converter
import de.mm20.launcher2.unitconverter.converters.CurrencyConverter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -13,6 +20,11 @@ import org.koin.core.component.inject
class UnitConverterSettingsScreenVM: ViewModel(), KoinComponent { class UnitConverterSettingsScreenVM: ViewModel(), KoinComponent {
private val settings: UnitConverterSettings by inject() private val settings: UnitConverterSettings by inject()
private val repository: UnitConverterRepository by inject()
val loading = mutableStateOf(false)
val convertersList = mutableStateOf(emptyList<Converter>())
val currenciesList = mutableStateOf(emptyList<String>())
val unitConverter = settings.enabled val unitConverter = settings.enabled
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
@ -25,4 +37,18 @@ class UnitConverterSettingsScreenVM: ViewModel(), KoinComponent {
fun setCurrencyConverter(currencyConverter: Boolean) { fun setCurrencyConverter(currencyConverter: Boolean) {
settings.setCurrencies(currencyConverter) settings.setCurrencies(currencyConverter)
} }
fun loadCurrencies() {
loading.value = true
viewModelScope.launch(Dispatchers.Default) {
convertersList.value = repository.availableConverters(
settings.currencies.distinctUntilChanged().first()
)
val currencyConverter = convertersList.value.find { it is CurrencyConverter }
if (currencyConverter != null) {
currenciesList.value = (currencyConverter as CurrencyConverter).getAbbreviations()
}
}
loading.value = false
}
} }

View File

@ -608,6 +608,7 @@
<string name="preference_search_unitconverter_summary">Usage: 1.5 kg or 4 cm &gt;&gt; in</string> <string name="preference_search_unitconverter_summary">Usage: 1.5 kg or 4 cm &gt;&gt; in</string>
<string name="preference_search_currencyconverter">Currency converter</string> <string name="preference_search_currencyconverter">Currency converter</string>
<string name="preference_search_currencyconverter_summary">Periodically download exchange rates to convert currencies</string> <string name="preference_search_currencyconverter_summary">Periodically download exchange rates to convert currencies</string>
<string name="preference_search_supportedunits">Supported units\n(use abbreviation on search box)</string>
<string name="preference_search_wikipedia">Wikipedia</string> <string name="preference_search_wikipedia">Wikipedia</string>
<string name="preference_search_wikipedia_summary">Search the free encyclopedia</string> <string name="preference_search_wikipedia_summary">Search the free encyclopedia</string>
<string name="preference_search_websites">Websites</string> <string name="preference_search_websites">Websites</string>

View File

@ -3,6 +3,20 @@
<!-- <!--
Important note: Unit symbols may not contain spaces. Important note: Unit symbols may not contain spaces.
--> -->
<!-- DIMENSIONS -->
<string name="dimension_length">Length</string>
<string name="dimension_mass">Mass</string>
<string name="dimension_velocity">Velocity</string>
<string name="dimension_volume">Volume</string>
<string name="dimension_area">Area</string>
<string name="dimension_currency">Currency</string>
<string name="dimension_data">Data</string>
<string name="dimension_bitrate">Bitrate</string>
<string name="dimension_pressure">Pressure</string>
<string name="dimension_energy">Energy</string>
<string name="dimension_frequency">Frequency</string>
<string name="dimension_temperature">Temperature</string>
<string name="dimension_time">Time</string>
<!-- UNITS OF LENGTH --> <!-- UNITS OF LENGTH -->
<string name="unit_meter_symbol">m</string> <string name="unit_meter_symbol">m</string>
<plurals name="unit_meter"> <plurals name="unit_meter">

View File

@ -161,6 +161,12 @@ class CurrencyRepository(
} }
} }
suspend fun getKnownUnits(): List<String> {
return withContext(Dispatchers.IO) {
AppDatabase.getInstance(context).currencyDao().getAllCurrencies().map { it.symbol }
}
}
suspend fun isValidCurrency(symbol: String): Boolean { suspend fun isValidCurrency(symbol: String): Boolean {
val isoSymbol = currencySymbolAliases[symbol] ?: symbol val isoSymbol = currencySymbolAliases[symbol] ?: symbol
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {

View File

@ -1,17 +1,19 @@
package de.mm20.launcher2.unitconverter package de.mm20.launcher2.unitconverter
enum class Dimension { import androidx.annotation.StringRes
Length,
Mass, enum class Dimension(@StringRes val resource: Int) {
Velocity, Length(R.string.dimension_length),
Volume, Mass(R.string.dimension_mass),
Area, Velocity(R.string.dimension_velocity),
Currency, Volume(R.string.dimension_volume),
Data, Area(R.string.dimension_area),
Bitrate, Currency(R.string.dimension_currency),
Pressure, Data(R.string.dimension_data),
Energy, Bitrate(R.string.dimension_bitrate),
Frequency, Pressure(R.string.dimension_pressure),
Temperature, Energy(R.string.dimension_energy),
Time Frequency(R.string.dimension_frequency),
Temperature(R.string.dimension_temperature),
Time(R.string.dimension_time),
} }

View File

@ -5,6 +5,7 @@ import de.mm20.launcher2.currencies.CurrencyRepository
import de.mm20.launcher2.preferences.search.UnitConverterSettings import de.mm20.launcher2.preferences.search.UnitConverterSettings
import de.mm20.launcher2.search.data.UnitConverter import de.mm20.launcher2.search.data.UnitConverter
import de.mm20.launcher2.unitconverter.converters.AreaConverter import de.mm20.launcher2.unitconverter.converters.AreaConverter
import de.mm20.launcher2.unitconverter.converters.Converter
import de.mm20.launcher2.unitconverter.converters.CurrencyConverter import de.mm20.launcher2.unitconverter.converters.CurrencyConverter
import de.mm20.launcher2.unitconverter.converters.DataConverter import de.mm20.launcher2.unitconverter.converters.DataConverter
import de.mm20.launcher2.unitconverter.converters.LengthConverter import de.mm20.launcher2.unitconverter.converters.LengthConverter
@ -25,6 +26,7 @@ import org.koin.core.component.KoinComponent
interface UnitConverterRepository { interface UnitConverterRepository {
fun search(query: String): Flow<UnitConverter?> fun search(query: String): Flow<UnitConverter?>
fun availableConverters(includeCurrencies: Boolean) : List<Converter>
} }
internal class UnitConverterRepositoryImpl( internal class UnitConverterRepositoryImpl(
@ -53,11 +55,29 @@ internal class UnitConverterRepositoryImpl(
} }
} }
override fun availableConverters(includeCurrencies: Boolean) : List<Converter> {
val converters = mutableListOf(
MassConverter(context),
LengthConverter(context),
DataConverter(context),
TimeConverter(context),
VelocityConverter(context),
AreaConverter(context),
TemperatureConverter(context)
)
if (includeCurrencies) converters.add(CurrencyConverter(currencyRepository))
return converters
}
private suspend fun queryUnitConverter( private suspend fun queryUnitConverter(
query: String, query: String,
includeCurrencies: Boolean includeCurrencies: Boolean
): UnitConverter? { ): UnitConverter? {
if (!query.matches(Regex("[0-9,.:]+ [^\\s]+")) && !query.matches(Regex("[0-9,.:]+ [^\\s]+ >> [^\\s]+"))) return null if (!query.matches(Regex("[0-9,.:]+ [^\\s]+")) &&
!query.matches(Regex("[0-9,.:]+ [^\\s]+ >> [^\\s]+")) &&
!query.matches(Regex("[0-9,.:]+ [^\\s]+ > [^\\s]+")) &&
!query.matches(Regex("[0-9,.:]+ [^\\s]+ - [^\\s]+"))) return null
val valueStr: String val valueStr: String
val unitStr: String val unitStr: String
val targetUnitStr: String? val targetUnitStr: String?
@ -70,17 +90,7 @@ internal class UnitConverterRepositoryImpl(
val value = valueStr.toDoubleOrNull() ?: valueStr.replace(',', '.').toDoubleOrNull() val value = valueStr.toDoubleOrNull() ?: valueStr.replace(',', '.').toDoubleOrNull()
?: return null ?: return null
val converters = mutableListOf( val converters = availableConverters(includeCurrencies)
MassConverter(context),
LengthConverter(context),
DataConverter(context),
TimeConverter(context),
VelocityConverter(context),
AreaConverter(context),
TemperatureConverter(context)
)
if (includeCurrencies) converters.add(CurrencyConverter(currencyRepository))
for (converter in converters) { for (converter in converters) {
if (!converter.isValidUnit(unitStr)) continue if (!converter.isValidUnit(unitStr)) continue

View File

@ -10,8 +10,8 @@ import de.mm20.launcher2.unitconverter.Dimension
import de.mm20.launcher2.unitconverter.UnitValue import de.mm20.launcher2.unitconverter.UnitValue
import java.text.DecimalFormat import java.text.DecimalFormat
import java.util.Locale import java.util.Locale
import java.util.Currency as JCurrency
import kotlin.math.abs import kotlin.math.abs
import java.util.Currency as JCurrency
class CurrencyConverter( class CurrencyConverter(
private val repository: CurrencyRepository, private val repository: CurrencyRepository,
@ -19,6 +19,10 @@ class CurrencyConverter(
override val dimension: Dimension = Dimension.Currency override val dimension: Dimension = Dimension.Currency
suspend fun getAbbreviations() : List<String> {
return repository.getKnownUnits()
}
private val topCurrencies = arrayOf("USD", "EUR", "JPY", "GBP", "AUD") private val topCurrencies = arrayOf("USD", "EUR", "JPY", "GBP", "AUD")

View File

@ -7,7 +7,7 @@ import de.mm20.launcher2.unitconverter.*
class TemperatureConverter(context: Context) : Converter { class TemperatureConverter(context: Context) : Converter {
override val dimension = Dimension.Temperature override val dimension = Dimension.Temperature
private val units = listOf( val units = listOf(
TemperatureMeasureUnit( TemperatureMeasureUnit(
context.getString(R.string.unit_degree_celsius_symbol), context.getString(R.string.unit_degree_celsius_symbol),
R.plurals.unit_degree_celsius, R.plurals.unit_degree_celsius,
@ -102,14 +102,14 @@ class TemperatureConverter(context: Context) : Converter {
} }
} }
private data class TemperatureMeasureUnit( data class TemperatureMeasureUnit(
override val symbol: String, override val symbol: String,
override val nameResource: Int, override val nameResource: Int,
val unit: TemperatureUnit val unit: TemperatureUnit
) : ) :
MeasureUnit MeasureUnit
private enum class TemperatureUnit { enum class TemperatureUnit {
DegreeCelsius, DegreeCelsius,
DegreeFahrenheit, DegreeFahrenheit,
Kelvin, Kelvin,

View File

@ -14,6 +14,8 @@ Examples:
You can also specify a target unit like this: You can also specify a target unit like this:
- `14 ft >> m` - `14 ft >> m`
- `14 ft > m`
- `14 ft - m`
If you don't specify a target unit, all supported units in the dimension of the input unit are returned. If you don't specify a target unit, all supported units in the dimension of the input unit are returned.