Use measurement system preference for location search
This commit is contained in:
@@ -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>({
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
141
app/ui/src/main/java/de/mm20/launcher2/ui/utils/Utils.kt
Normal file
141
app/ui/src/main/java/de/mm20/launcher2/ui/utils/Utils.kt
Normal 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)}"
|
||||
}
|
||||
}
|
||||
@@ -166,7 +166,6 @@ data class LauncherSettingsData internal constructor(
|
||||
@Deprecated("Use locationSearchProviders instead")
|
||||
val locationSearchEnabled: Boolean = false,
|
||||
val locationSearchProviders: Set<String> = setOf("openstreetmaps"),
|
||||
val locationSearchImperialUnits: Boolean = false,
|
||||
val locationSearchRadius: Int = 1500,
|
||||
val locationSearchHideUncategorized: Boolean = true,
|
||||
val locationSearchOverpassUrl: String? = null,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package de.mm20.launcher2.preferences.search
|
||||
|
||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||
import de.mm20.launcher2.preferences.MeasurementSystem
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
@@ -15,7 +16,7 @@ class LocationSearchSettings internal constructor(
|
||||
hideUncategorized = it.locationSearchHideUncategorized,
|
||||
overpassUrl = it.locationSearchOverpassUrl,
|
||||
tileServer = it.locationSearchTileServer,
|
||||
imperialUnits = it.locationSearchImperialUnits,
|
||||
measurementSystem = it.localeMeasurementSystem,
|
||||
showMap = it.locationSearchShowMap,
|
||||
showPositionOnMap = it.locationSearchShowPositionOnMap,
|
||||
themeMap = it.locationSearchThemeMap,
|
||||
@@ -130,14 +131,8 @@ class LocationSearchSettings internal constructor(
|
||||
}
|
||||
}
|
||||
|
||||
val imperialUnits
|
||||
get() = launcherDataStore.data.map { it.locationSearchImperialUnits }
|
||||
|
||||
fun setImperialUnits(imperialUnits: Boolean) {
|
||||
launcherDataStore.update {
|
||||
it.copy(locationSearchImperialUnits = imperialUnits)
|
||||
}
|
||||
}
|
||||
val measurementSystem
|
||||
get() = launcherDataStore.data.map { it.localeMeasurementSystem }
|
||||
|
||||
companion object {
|
||||
const val DefaultTileServerUrl = "https://tile.openstreetmap.org/\${z}/\${x}/\${y}.png"
|
||||
@@ -152,7 +147,7 @@ data class LocationSearchSettingsData(
|
||||
val hideUncategorized: Boolean = true,
|
||||
val overpassUrl: String? = null,
|
||||
val tileServer: String? = null,
|
||||
val imperialUnits: Boolean = false,
|
||||
val measurementSystem: MeasurementSystem = MeasurementSystem.System,
|
||||
val showMap: Boolean = false,
|
||||
val showPositionOnMap: Boolean = false,
|
||||
val themeMap: Boolean = true,
|
||||
|
||||
Reference in New Issue
Block a user