Use measurement system preference for location search

This commit is contained in:
MM20
2025-11-29 23:02:03 +01:00
parent 4e05d845b2
commit 6d1be70837
9 changed files with 202 additions and 89 deletions

View File

@@ -23,46 +23,6 @@ fun Float.toDp(): Dp {
return (this / LocalDensity.current.density).dp
}
fun Float.roundToString(): String = this.roundToInt().toString()
fun Float.metersToLocalizedString(context: Context, imperialUnits: Boolean): String {
val decimalFormat =
DecimalFormat().apply { maximumFractionDigits = 1; minimumFractionDigits = 0 }
val (value, unit) = if (imperialUnits) {
// yee haw
val asFeet = this * 3.28084f
val isYards = asFeet >= 3f
val isMiles = asFeet >= 5280f
val value =
if (isMiles) decimalFormat.format(asFeet / 5280f)
else if (isYards) (asFeet / 3f).roundToString()
else asFeet.roundToString()
val unit = context.getString(
if (isMiles) R.string.unit_mile_symbol
else if (isYards) R.string.unit_yard_symbol
else R.string.unit_foot_symbol
)
value to unit
} else {
val isKm = this >= 1000f
val value =
if (isKm) decimalFormat.format(this / 1000f)
else this.roundToString()
val unit = context.getString(
if (isKm) R.string.unit_kilometer_symbol
else R.string.unit_meter_symbol
)
value to unit
}
return "$value $unit"
}
// https://stackoverflow.com/a/68651222
val Float.Companion.DegreesConverter
get() = TwoWayConverter<Float, AnimationVector2D>({

View File

@@ -16,6 +16,7 @@ import de.mm20.launcher2.notifications.Notification
import de.mm20.launcher2.notifications.NotificationRepository
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.MeasurementSystem
import de.mm20.launcher2.preferences.search.ContactSearchSettings
import de.mm20.launcher2.preferences.search.LocationSearchSettings
import de.mm20.launcher2.search.AppShortcut
@@ -236,8 +237,8 @@ class SearchableItemVM : ListItemViewModel(), KoinComponent {
permissionsManager.requestPermission(activity, PermissionGroup.AppShortcuts)
}
val imperialUnits = locationSearchSettings.imperialUnits
.stateIn(viewModelScope, SharingStarted.Lazily, false)
val measurementSystem = locationSearchSettings.measurementSystem
.stateIn(viewModelScope, SharingStarted.Lazily, MeasurementSystem.Metric)
val showMap = locationSearchSettings.showMap
.stateIn(viewModelScope, SharingStarted.Lazily, false)

View File

@@ -90,6 +90,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import blend.Blend.harmonize
import coil.compose.AsyncImage
import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.preferences.MeasurementSystem
import de.mm20.launcher2.search.Location
import de.mm20.launcher2.search.isOpen
import de.mm20.launcher2.search.location.Attribution
@@ -110,7 +111,6 @@ import de.mm20.launcher2.ui.component.Toolbar
import de.mm20.launcher2.ui.component.ToolbarAction
import de.mm20.launcher2.ui.ktx.atTone
import de.mm20.launcher2.ui.ktx.blendIntoViewScale
import de.mm20.launcher2.ui.ktx.metersToLocalizedString
import de.mm20.launcher2.ui.ktx.toComposeColor
import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
import de.mm20.launcher2.ui.launcher.search.listItemViewModel
@@ -119,6 +119,7 @@ import de.mm20.launcher2.ui.locals.LocalDarkTheme
import de.mm20.launcher2.ui.locals.LocalFavoritesEnabled
import de.mm20.launcher2.ui.locals.LocalGridSettings
import de.mm20.launcher2.ui.modifier.scale
import de.mm20.launcher2.ui.utils.formatDistance
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.emptyFlow
import java.time.Duration
@@ -161,7 +162,7 @@ fun LocationItem(
val icon by viewModel.icon.collectAsStateWithLifecycle()
val badge by viewModel.badge.collectAsStateWithLifecycle(null)
val imperialUnits by viewModel.imperialUnits.collectAsState()
val measurementSystem by viewModel.measurementSystem.collectAsState()
val showMap by viewModel.showMap.collectAsState()
@@ -201,8 +202,13 @@ fun LocationItem(
this@AnimatedContent
)
)
val formattedDistance =
distance?.metersToLocalizedString(context, imperialUnits)
val formattedDistance = distance?.let {
formatDistance(
context,
it,
measurementSystem
)
}
val sublabel = listOf(location.category, formattedDistance)
.fastFilterNotNull()
.joinToString("")
@@ -313,9 +319,13 @@ fun LocationItem(
) {
val category = location.category
val acceptedPaymentMethods = location.acceptedPaymentMethods
val formattedDistance = distance?.metersToLocalizedString(
context, imperialUnits
)
val formattedDistance = distance?.let {
formatDistance(
context,
it,
measurementSystem
)
}
if (category != null || formattedDistance != null) {
Text(
when {
@@ -1314,4 +1324,4 @@ private fun departureInMinutes(context: Context, departure: Departure): String {
timeLeft
)
}
}
}

View File

@@ -132,7 +132,7 @@ class WeatherWidgetVM : ViewModel(), KoinComponent {
}
return@map ms
}
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), MeasurementSystem.System)
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), MeasurementSystem.Metric)
fun selectDay(index: Int) {
selectedDayIndex = min(index, forecasts.lastIndex)

View File

@@ -1,6 +1,10 @@
package de.mm20.launcher2.ui.settings.locations
import android.app.PendingIntent
import android.content.Context
import android.icu.number.NumberFormatter
import android.icu.util.Measure
import android.icu.util.MeasureUnit
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.MaterialTheme
@@ -17,25 +21,26 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation3.runtime.NavKey
import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.ktx.sendWithBackgroundPermission
import de.mm20.launcher2.plugin.PluginState
import de.mm20.launcher2.preferences.MeasurementSystem
import de.mm20.launcher2.preferences.search.LocationSearchSettings
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.preferences.GuardedPreference
import de.mm20.launcher2.ui.component.preferences.ListPreference
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
import de.mm20.launcher2.ui.component.preferences.PreferenceWithSwitch
import de.mm20.launcher2.ui.component.preferences.SliderPreference
import de.mm20.launcher2.ui.component.preferences.SwitchPreference
import de.mm20.launcher2.ui.component.preferences.TextPreference
import de.mm20.launcher2.ui.ktx.metersToLocalizedString
import de.mm20.launcher2.ui.locals.LocalBackStack
import de.mm20.launcher2.ui.settings.osm.OsmSettingsRoute
import de.mm20.launcher2.ui.utils.formatDistance
import kotlinx.serialization.Serializable
@Serializable
data object LocationsSettingsRoute: NavKey
data object LocationsSettingsRoute : NavKey
@Composable
fun LocationsSettingsScreen() {
@@ -45,7 +50,7 @@ fun LocationsSettingsScreen() {
val context = LocalContext.current
val osmLocations by viewModel.osmLocations.collectAsState()
val imperialUnits by viewModel.imperialUnits.collectAsState()
val measurementSystem by viewModel.measurementSystem.collectAsState()
val radius by viewModel.radius.collectAsState()
val showMap by viewModel.showMap.collectAsState()
val themeMap by viewModel.themeMap.collectAsState()
@@ -109,18 +114,6 @@ fun LocationsSettingsScreen() {
}
item {
PreferenceCategory {
ListPreference(
title = stringResource(R.string.length_unit),
items = listOf(
stringResource(R.string.imperial) to true,
stringResource(R.string.metric) to false
),
enabled = anyLocationProviderEnabled,
value = imperialUnits,
onValueChanged = {
viewModel.setImperialUnits(it)
}
)
SliderPreference(
title = stringResource(R.string.preference_search_locations_radius),
value = radius,
@@ -136,12 +129,9 @@ fun LocationsSettingsScreen() {
modifier = Modifier
.width(64.dp)
.padding(start = 16.dp),
text = it.toFloat()
.metersToLocalizedString(LocalContext.current, imperialUnits),
text = formatDistance(context, it.toFloat(), measurementSystem),
style = MaterialTheme.typography.titleSmall
)
it.toFloat()
.metersToLocalizedString(LocalContext.current, imperialUnits)
}
)
}
@@ -185,4 +175,5 @@ fun LocationsSettingsScreen() {
}
}
}
}
}

View File

@@ -1,11 +1,16 @@
package de.mm20.launcher2.ui.settings.locations
import android.icu.util.LocaleData
import android.icu.util.ULocale
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.plugin.PluginType
import de.mm20.launcher2.plugins.PluginService
import de.mm20.launcher2.preferences.MeasurementSystem
import de.mm20.launcher2.preferences.search.LocationSearchSettings
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
@@ -27,12 +32,23 @@ class LocationsSettingsScreenVM : ViewModel(), KoinComponent {
settings.setOsmLocations(osmLocations)
}
val imperialUnits = settings.imperialUnits
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)
fun setImperialUnits(imperialUnits: Boolean) {
settings.setImperialUnits(imperialUnits)
}
val measurementSystem = settings.measurementSystem
.map { ms ->
if (ms == MeasurementSystem.System) {
return@map if (isAtLeastApiLevel(28)) {
val systemMs = LocaleData.getMeasurementSystem(ULocale.getDefault())
when(systemMs) {
LocaleData.MeasurementSystem.UK -> MeasurementSystem.UnitedKingdom
LocaleData.MeasurementSystem.US -> MeasurementSystem.UnitedStates
else -> MeasurementSystem.Metric
}
} else {
MeasurementSystem.Metric
}
}
return@map ms
}
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), MeasurementSystem.Metric)
val radius = settings.searchRadius
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), 1500)

View File

@@ -0,0 +1,141 @@
package de.mm20.launcher2.ui.utils
import android.content.Context
import android.icu.number.NumberFormatter
import android.icu.number.Precision
import android.icu.util.Measure
import android.icu.util.MeasureUnit
import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.preferences.MeasurementSystem
import de.mm20.launcher2.ui.R
import java.text.NumberFormat
import java.util.Locale
internal fun formatDistance(
context: Context,
meters: Float,
measurementSystem: MeasurementSystem,
): String {
when (measurementSystem) {
MeasurementSystem.UnitedStates -> {
val feet = meters * 3.28084f
if (feet >= 2640) {
return formatMiles(context, feet / 5280f)
}
return formatFeet(context, feet)
}
MeasurementSystem.UnitedKingdom -> {
val yards = meters * 1.09361f
if (yards >= 880) {
return formatMiles(context, yards / 1760f)
}
return formatYards(context, yards)
}
else -> {
if (meters >= 1000) {
return formatKilometers(context, meters / 1000f)
}
return formatMeters(context, meters)
}
}
}
private fun formatMeters(
context: Context,
meters: Float,
): String {
if (isAtLeastApiLevel(30)) {
return NumberFormatter
.withLocale(
Locale.getDefault()
).unitWidth(NumberFormatter.UnitWidth.NARROW)
.precision(Precision.maxFraction(0))
.format(Measure(meters, MeasureUnit.METER)).toString()
} else {
val formatted = NumberFormat.getInstance().apply {
maximumFractionDigits = 0
}.format(meters)
return "$formatted ${context.getString(R.string.unit_meter_symbol)}"
}
}
private fun formatKilometers(
context: Context,
kilometers: Float,
): String {
if (isAtLeastApiLevel(30)) {
return NumberFormatter
.withLocale(
Locale.getDefault()
).unitWidth(NumberFormatter.UnitWidth.NARROW)
.precision(Precision.maxFraction(1))
.format(Measure(kilometers, MeasureUnit.KILOMETER)).toString()
} else {
val formatted = NumberFormat.getInstance().apply {
maximumFractionDigits = 1
}.format(kilometers)
return "$formatted ${context.getString(R.string.unit_kilometer_symbol)}"
}
}
private fun formatFeet(
context: Context,
feet: Float,
): String {
if (isAtLeastApiLevel(30)) {
return NumberFormatter
.withLocale(
Locale.getDefault()
).unitWidth(NumberFormatter.UnitWidth.NARROW)
.precision(Precision.maxFraction(0))
.format(Measure(feet, MeasureUnit.FOOT)).toString()
} else {
val formatted = NumberFormat.getInstance().apply {
maximumFractionDigits = 0
}.format(feet)
return "$formatted ${context.getString(R.string.unit_foot_symbol)}"
}
}
private fun formatYards(
context: Context,
yards: Float,
): String {
if (isAtLeastApiLevel(30)) {
return NumberFormatter
.withLocale(
Locale.getDefault()
).unitWidth(NumberFormatter.UnitWidth.NARROW)
.precision(Precision.maxFraction(0))
.format(Measure(yards, MeasureUnit.YARD)).toString()
} else {
val formatted = NumberFormat.getInstance().apply {
maximumFractionDigits = 0
}.format(yards)
return "$formatted ${context.getString(R.string.unit_yard_symbol)}"
}
}
private fun formatMiles(
context: Context,
miles: Float,
): String {
if (isAtLeastApiLevel(30)) {
return NumberFormatter
.withLocale(
Locale.getDefault()
).unitWidth(NumberFormatter.UnitWidth.NARROW)
.precision(Precision.maxFraction(1))
.format(Measure(miles, MeasureUnit.MILE)).toString()
} else {
val formatted = NumberFormat.getInstance().apply {
maximumFractionDigits = 1
}.format(miles)
return "$formatted ${context.getString(R.string.unit_mile_symbol)}"
}
}