Move unit converter supported units to a different screen

This commit is contained in:
MM20 2024-08-12 19:34:38 +02:00
parent 827eb08908
commit 3887324d37
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
19 changed files with 236 additions and 116 deletions

View File

@ -66,6 +66,7 @@ import de.mm20.launcher2.ui.settings.plugins.PluginsSettingsScreen
import de.mm20.launcher2.ui.settings.search.SearchSettingsScreen
import de.mm20.launcher2.ui.settings.searchactions.SearchActionsSettingsScreen
import de.mm20.launcher2.ui.settings.tags.TagsSettingsScreen
import de.mm20.launcher2.ui.settings.unitconverter.UnitConverterHelpSettingsScreen
import de.mm20.launcher2.ui.settings.unitconverter.UnitConverterSettingsScreen
import de.mm20.launcher2.ui.settings.weather.WeatherIntegrationSettingsScreen
import de.mm20.launcher2.ui.settings.wikipedia.WikipediaSettingsScreen
@ -169,6 +170,9 @@ class SettingsActivity : BaseActivity() {
composable("settings/search/unitconverter") {
UnitConverterSettingsScreen()
}
composable("settings/search/unitconverter/help") {
UnitConverterHelpSettingsScreen()
}
composable("settings/search/wikipedia") {
WikipediaSettingsScreen()
}

View File

@ -0,0 +1,100 @@
package de.mm20.launcher2.ui.settings.unitconverter
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.preferences.Preference
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
import de.mm20.launcher2.ui.launcher.search.unitconverter.getDimensionIcon
import de.mm20.launcher2.unitconverter.Dimension
import de.mm20.launcher2.unitconverter.MeasureUnit
import kotlinx.coroutines.flow.flow
@Composable
fun UnitConverterHelpSettingsScreen() {
val viewModel: UnitConverterSettingsScreenVM = viewModel()
val availableConverters by viewModel.availableConverters.collectAsState(emptyList())
val availableUnits by viewModel.availableUnits.collectAsState(emptyList())
PreferenceScreen(title = stringResource(R.string.preference_search_unitconverter),
helpUrl = "https://kvaesitso.mm20.de/docs/user-guide/search/unit-converter") {
for (i in availableConverters.indices) {
stickyHeader {
DimensionHeader(availableConverters[i].dimension)
}
items(availableUnits.getOrNull(i)?.size ?: 0) {
val unit = availableUnits[i].getOrNull(it) ?: return@items
Preference(
title = unit.formatName(LocalContext.current, 1.0),
controls = {
Box(
modifier = Modifier.size(36.dp)
.background(MaterialTheme.colorScheme.secondaryContainer, MaterialTheme.shapes.extraSmall),
contentAlignment = Alignment.Center,
) {
Text(unit.symbol, style = MaterialTheme.typography.labelSmall)
}
}
)
}
}
}
}
@Composable
private fun DimensionHeader(dimension: Dimension) {
Row(
modifier = Modifier.background(MaterialTheme.colorScheme.surfaceContainer).fillMaxWidth().padding(top = 16.dp, bottom = 16.dp)
) {
Icon(
getDimensionIcon(dimension),
null,
modifier = Modifier.padding(horizontal = 24.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
Text(
stringResource(dimension.resource),
style = MaterialTheme.typography.titleSmall,
modifier = Modifier.weight(1f),
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
@Composable
private fun UnitList(units: List<MeasureUnit>) {
val context = LocalContext.current
for (unit in units) {
Preference(
title = unit.formatName(context, 1.0),
controls = {
Box(
modifier = Modifier.size(36.dp)
.background(MaterialTheme.colorScheme.secondaryContainer, MaterialTheme.shapes.extraSmall),
contentAlignment = Alignment.Center,
) {
Text(unit.symbol, style = MaterialTheme.typography.labelSmall)
}
}
)
}
}

View File

@ -1,49 +1,28 @@
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.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Help
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.preferences.search.UnitConverterSettings
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.PreferenceScreen
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
import de.mm20.launcher2.ui.locals.LocalNavController
@Composable
fun UnitConverterSettingsScreen() {
val settings: UnitConverterSettings by inject()
val viewModel: UnitConverterSettingsScreenVM = viewModel()
val loading by viewModel.loading
val navController = LocalNavController.current
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()
)
}
}
PreferenceScreen(
title = stringResource(R.string.preference_search_unitconverter),
helpUrl = "https://kvaesitso.mm20.de/docs/user-guide/search/unit-converter"
) {
item {
PreferenceCategory {
val unitConverter by viewModel.unitConverter.collectAsState()
@ -66,46 +45,16 @@ 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)")
}
}
}
PreferenceCategory {
Preference(
title = stringResource(R.string.preference_search_supportedunits),
icon = Icons.AutoMirrored.Default.Help,
onClick = {
navController?.navigate("settings/search/unitconverter/help")
}
Preference(
title = stringResource(converter.dimension.resource),
icon = getDimensionIcon(converter.dimension),
summary = units
)
}
)
}
}
}
}
}

View File

@ -6,12 +6,13 @@ import androidx.lifecycle.viewModelScope
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.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
@ -22,10 +23,6 @@ class UnitConverterSettingsScreenVM: ViewModel(), KoinComponent {
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
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setUnitConverter(unitConverter: Boolean) {
@ -37,18 +34,12 @@ class UnitConverterSettingsScreenVM: ViewModel(), KoinComponent {
fun setCurrencyConverter(currencyConverter: Boolean) {
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
}
val availableConverters = settings.currencies.map {
repository.getAvailableConverters(includeCurrencies = it)
}.shareIn(viewModelScope, SharingStarted.WhileSubscribed(100), 1)
val availableUnits = availableConverters.map {
it.map { converter -> converter.getSupportedUnits() }
}.shareIn(viewModelScope, SharingStarted.WhileSubscribed(100), 1)
}

View File

@ -608,7 +608,7 @@
<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_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_supportedunits">Supported units</string>
<string name="preference_search_wikipedia">Wikipedia</string>
<string name="preference_search_wikipedia_summary">Search the free encyclopedia</string>
<string name="preference_search_websites">Websites</string>

View File

@ -8,9 +8,7 @@ import kotlin.math.roundToInt
internal object ConverterUtils {
fun formatName(context: Context, unit: MeasureUnit, value: Double): String {
val resId = unit.nameResource
val text = context.resources.getQuantityString(resId, value.roundToInt())
return text
return unit.formatName(context, value)
}
fun formatValue(context: Context, unit: MeasureUnit, value: Double): String {

View File

@ -0,0 +1,8 @@
package de.mm20.launcher2.unitconverter
import android.content.Context
interface MeasureUnit {
val symbol: String
fun formatName(context: Context, value: Double): String
}

View File

@ -1,6 +0,0 @@
package de.mm20.launcher2.unitconverter
interface MeasureUnit {
val symbol: String
val nameResource: Int
}

View File

@ -19,6 +19,7 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
@ -26,7 +27,7 @@ import org.koin.core.component.KoinComponent
interface UnitConverterRepository {
fun search(query: String): Flow<UnitConverter?>
fun availableConverters(includeCurrencies: Boolean) : List<Converter>
suspend fun getAvailableConverters(includeCurrencies: Boolean) : List<Converter>
}
internal class UnitConverterRepositoryImpl(
@ -55,7 +56,7 @@ internal class UnitConverterRepositoryImpl(
}
}
override fun availableConverters(includeCurrencies: Boolean) : List<Converter> {
override suspend fun getAvailableConverters(includeCurrencies: Boolean) : List<Converter> {
val converters = mutableListOf(
MassConverter(context),
LengthConverter(context),
@ -90,7 +91,7 @@ internal class UnitConverterRepositoryImpl(
val value = valueStr.toDoubleOrNull() ?: valueStr.replace(',', '.').toDoubleOrNull()
?: return null
val converters = availableConverters(includeCurrencies)
val converters = getAvailableConverters(includeCurrencies)
for (converter in converters) {
if (!converter.isValidUnit(unitStr)) continue

View File

@ -4,7 +4,7 @@ import android.content.Context
import de.mm20.launcher2.unitconverter.Dimension
import de.mm20.launcher2.unitconverter.R
class AreaConverter(context: Context) : SimpleFactorConverter() {
internal class AreaConverter(context: Context) : SimpleFactorConverter() {
override val dimension = Dimension.Area
override val standardUnits = listOf(

View File

@ -3,6 +3,7 @@ package de.mm20.launcher2.unitconverter.converters
import android.content.Context
import de.mm20.launcher2.search.data.UnitConverter
import de.mm20.launcher2.unitconverter.Dimension
import de.mm20.launcher2.unitconverter.MeasureUnit
interface Converter {
val dimension: Dimension
@ -15,5 +16,7 @@ interface Converter {
value: Double,
toUnit: String?
): UnitConverter
suspend fun getSupportedUnits(): List<MeasureUnit>
}

View File

@ -7,6 +7,7 @@ import de.mm20.launcher2.currencies.CurrencyRepository
import de.mm20.launcher2.search.data.CurrencyUnitConverter
import de.mm20.launcher2.search.data.UnitConverter
import de.mm20.launcher2.unitconverter.Dimension
import de.mm20.launcher2.unitconverter.MeasureUnit
import de.mm20.launcher2.unitconverter.UnitValue
import java.text.DecimalFormat
import java.util.Locale
@ -19,7 +20,7 @@ class CurrencyConverter(
override val dimension: Dimension = Dimension.Currency
suspend fun getAbbreviations() : List<String> {
suspend fun getAbbreviations(): List<String> {
return repository.getKnownUnits()
}
@ -35,7 +36,14 @@ class CurrencyConverter(
val text = StringBuilder()
val currency = Currency.getInstance(symbol) ?: return formatNameFallback(symbol)
val pluralCount = PluralRules.forLocale(Locale.getDefault()).select(value)
text.append(currency.getName(Locale.getDefault(), Currency.PLURAL_LONG_NAME, pluralCount, booleanArrayOf(false)))
text.append(
currency.getName(
Locale.getDefault(),
Currency.PLURAL_LONG_NAME,
pluralCount,
booleanArrayOf(false)
)
)
return text.toString()
}
@ -62,12 +70,22 @@ class CurrencyConverter(
}
override suspend fun convert(context: Context, fromUnit: String, value: Double, toUnit: String?): UnitConverter {
override suspend fun convert(
context: Context,
fromUnit: String,
value: Double,
toUnit: String?
): UnitConverter {
val fromIsoCode = repository.resolveAlias(fromUnit)
val toIsoCode = toUnit?.let { repository.resolveAlias(it) }
val values = repository.convertCurrency(fromIsoCode, value, toIsoCode).map {
UnitValue(it.second, it.first, formatName(it.first, it.second), formatValue(it.first, it.second))
UnitValue(
it.second,
it.first,
formatName(it.first, it.second),
formatValue(it.first, it.second)
)
}.toMutableList()
val ownCurrencySymbol = JCurrency.getInstance(Locale.getDefault()).currencyCode ?: "USD"
@ -89,8 +107,46 @@ class CurrencyConverter(
values.add(0, ownCurrency)
}
val inputValue = UnitValue(value, fromIsoCode, formatName(fromIsoCode, value), formatValue(fromIsoCode, value))
val inputValue = UnitValue(
value,
fromIsoCode,
formatName(fromIsoCode, value),
formatValue(fromIsoCode, value)
)
val lastUpdate = repository.getLastUpdate(fromIsoCode)
return CurrencyUnitConverter(dimension, inputValue, values, lastUpdate)
}
override suspend fun getSupportedUnits(): List<MeasureUnit> {
val currencies = repository.getKnownUnits()
return currencies.map {
CurrencyUnit(
symbol = it,
)
}
}
}
internal data class CurrencyUnit(
override val symbol: String,
) : MeasureUnit {
override fun formatName(context: Context, value: Double): String {
val text = StringBuilder()
val currency = try {
Currency.getInstance(symbol)
} catch (e: IllegalArgumentException) {
null
} ?: return symbol
val pluralCount = PluralRules.forLocale(Locale.getDefault()).select(value)
text.append(
currency.getName(
Locale.getDefault(),
Currency.LONG_NAME,
pluralCount,
booleanArrayOf(false)
)
)
return text.toString()
}
}

View File

@ -4,7 +4,7 @@ import android.content.Context
import de.mm20.launcher2.unitconverter.Dimension
import de.mm20.launcher2.unitconverter.R
class DataConverter(context: Context) : SimpleFactorConverter() {
internal class DataConverter(context: Context) : SimpleFactorConverter() {
override val dimension = Dimension.Data
override val standardUnits = listOf(

View File

@ -4,7 +4,7 @@ import android.content.Context
import de.mm20.launcher2.unitconverter.Dimension
import de.mm20.launcher2.unitconverter.R
class LengthConverter(context: Context) : SimpleFactorConverter() {
internal class LengthConverter(context: Context) : SimpleFactorConverter() {
override val dimension = Dimension.Length
override val standardUnits = listOf(

View File

@ -4,7 +4,7 @@ import android.content.Context
import de.mm20.launcher2.unitconverter.Dimension
import de.mm20.launcher2.unitconverter.R
class MassConverter(context: Context): SimpleFactorConverter() {
internal class MassConverter(context: Context): SimpleFactorConverter() {
override val dimension = Dimension.Mass
override val standardUnits = listOf(

View File

@ -5,11 +5,12 @@ import de.mm20.launcher2.search.data.UnitConverter
import de.mm20.launcher2.unitconverter.ConverterUtils
import de.mm20.launcher2.unitconverter.MeasureUnit
import de.mm20.launcher2.unitconverter.UnitValue
import kotlin.math.roundToInt
/**
* A converter for units that can converted into each other by simple multiplication with a constant factor
*/
abstract class SimpleFactorConverter: Converter {
internal abstract class SimpleFactorConverter: Converter {
open val standardUnits: List<MeasureUnitWithFactor> = emptyList()
/**
@ -36,11 +37,19 @@ abstract class SimpleFactorConverter: Converter {
val inputValue = UnitValue(value, fromUnit, ConverterUtils.formatName(context, unit, value), ConverterUtils.formatValue(context, unit, value))
return UnitConverter(dimension, inputValue, results)
}
override suspend fun getSupportedUnits(): List<MeasureUnit> {
return standardUnits
}
}
data class MeasureUnitWithFactor(
val factor: Double,
override val symbol: String,
override val nameResource: Int
): MeasureUnit
val nameResource: Int
): MeasureUnit {
override fun formatName(context: Context, value: Double): String {
return context.resources.getQuantityString(nameResource, value.roundToInt())
}
}

View File

@ -4,7 +4,7 @@ import android.content.Context
import de.mm20.launcher2.search.data.UnitConverter
import de.mm20.launcher2.unitconverter.*
class TemperatureConverter(context: Context) : Converter {
internal class TemperatureConverter(context: Context) : Converter {
override val dimension = Dimension.Temperature
val units = listOf(
@ -100,14 +100,21 @@ class TemperatureConverter(context: Context) : Converter {
}
throw IllegalArgumentException()
}
override suspend fun getSupportedUnits(): List<MeasureUnit> {
return units
}
}
data class TemperatureMeasureUnit(
override val symbol: String,
override val nameResource: Int,
val nameResource: Int,
val unit: TemperatureUnit
) :
MeasureUnit
) : MeasureUnit {
override fun formatName(context: Context, value: Double): String {
return context.resources.getQuantityString(nameResource, value.toInt())
}
}
enum class TemperatureUnit {
DegreeCelsius,

View File

@ -4,7 +4,7 @@ import android.content.Context
import de.mm20.launcher2.unitconverter.Dimension
import de.mm20.launcher2.unitconverter.R
class TimeConverter(context: Context) : SimpleFactorConverter() {
internal class TimeConverter(context: Context) : SimpleFactorConverter() {
override val dimension = Dimension.Time
override val standardUnits = listOf(

View File

@ -4,7 +4,7 @@ import android.content.Context
import de.mm20.launcher2.unitconverter.Dimension
import de.mm20.launcher2.unitconverter.R
class VelocityConverter(context: Context) : SimpleFactorConverter() {
internal class VelocityConverter(context: Context) : SimpleFactorConverter() {
override val dimension = Dimension.Velocity
override val standardUnits = listOf(