Add measurement system preference

This commit is contained in:
MM20
2025-11-29 20:49:54 +01:00
parent 0f45851c4d
commit 8094843972
12 changed files with 127 additions and 58 deletions

View File

@@ -70,6 +70,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.repeatOnLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.preferences.MeasurementSystem
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.common.WeatherLocationSearchDialog
import de.mm20.launcher2.ui.component.Banner
@@ -101,7 +102,7 @@ fun WeatherWidget(widget: WeatherWidget) {
val selectedForecast by viewModel.currentForecast
val imperialUnits by viewModel.imperialUnits.collectAsState(false)
val measurementSystem by viewModel.measurementSystem.collectAsState()
val compactMode = !widget.config.showForecast
val isProviderAvailable by viewModel.isProviderAvailable.collectAsStateWithLifecycle(true)
@@ -169,7 +170,7 @@ fun WeatherWidget(widget: WeatherWidget) {
}
CurrentWeather(forecast, imperialUnits)
CurrentWeather(forecast, measurementSystem)
if (!compactMode) {
@@ -187,7 +188,7 @@ fun WeatherWidget(widget: WeatherWidget) {
WeatherTimeSelector(
forecasts = currentDayForecasts,
selectedForecast = forecast,
imperialUnits = imperialUnits,
measurementSystem = measurementSystem,
onTimeSelected = {
viewModel.selectForecast(it)
},
@@ -200,7 +201,7 @@ fun WeatherWidget(widget: WeatherWidget) {
onDaySelected = {
viewModel.selectDay(it)
},
imperialUnits = imperialUnits,
measurementSystem = measurementSystem,
)
}
}
@@ -210,7 +211,7 @@ fun WeatherWidget(widget: WeatherWidget) {
}
@Composable
fun CurrentWeather(forecast: Forecast, imperialUnits: Boolean) {
fun CurrentWeather(forecast: Forecast, measurementSystem: MeasurementSystem) {
val context = LocalContext.current
val weatherApp = remember {
context.packageManager.resolveActivity(
@@ -219,6 +220,12 @@ fun CurrentWeather(forecast: Forecast, imperialUnits: Boolean) {
}, 0
)
}
val useFahrenheit = measurementSystem == MeasurementSystem.UnitedStates
val useInches = measurementSystem == MeasurementSystem.UnitedStates
val useMph = measurementSystem == MeasurementSystem.UnitedStates || measurementSystem == MeasurementSystem.UnitedKingdom
var bounds by remember { mutableStateOf(Rect.Zero) }
val view = LocalView.current
Column(
@@ -312,7 +319,7 @@ fun CurrentWeather(forecast: Forecast, imperialUnits: Boolean) {
Text(
modifier = Modifier.weight(1f),
text = convertTemperature(
imperialUnits = imperialUnits,
useFahrenheit = useFahrenheit,
temp = forecast.temperature
).toString() + "°",
style = MaterialTheme.typography.headlineMedium,
@@ -389,7 +396,7 @@ fun CurrentWeather(forecast: Forecast, imperialUnits: Boolean) {
Spacer(modifier = Modifier.padding(3.dp))
Text(
text = if (forecast.windSpeed != null) {
formatWindSpeed(imperialUnits, forecast)
formatWindSpeed(useMph, forecast)
} else {
windDirectionAsWord(forecast.windDirection!!)
},
@@ -413,7 +420,7 @@ fun CurrentWeather(forecast: Forecast, imperialUnits: Boolean) {
)
Spacer(modifier = Modifier.padding(3.dp))
Text(
text = formatPrecipitation(imperialUnits, forecast),
text = formatPrecipitation(useInches, forecast),
style = MaterialTheme.typography.bodySmall,
)
}
@@ -428,11 +435,13 @@ fun WeatherTimeSelector(
modifier: Modifier = Modifier,
forecasts: List<Forecast>,
selectedForecast: Forecast,
imperialUnits: Boolean,
measurementSystem: MeasurementSystem,
onTimeSelected: (Int) -> Unit
) {
val dateFormat = remember { DateFormat.getTimeInstance(DateFormat.SHORT) }
val useFahrenheit = measurementSystem == MeasurementSystem.UnitedStates
val listState = rememberLazyListState()
LazyRow(
state = listState,
@@ -494,7 +503,7 @@ fun WeatherTimeSelector(
Text(
modifier = Modifier
.alpha(alpha),
text = "${convertTemperature(imperialUnits, fc.temperature)}°",
text = "${convertTemperature(useFahrenheit, fc.temperature)}°",
softWrap = false,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
@@ -518,9 +527,10 @@ fun WeatherDaySelector(
days: List<DailyForecast>,
selectedDay: DailyForecast,
onDaySelected: (Int) -> Unit,
imperialUnits: Boolean
measurementSystem: MeasurementSystem
) {
val dateFormat = SimpleDateFormat("EEE")
val useFahrenheit = measurementSystem == MeasurementSystem.UnitedStates
val listState = rememberLazyListState()
LazyRow(
@@ -562,8 +572,8 @@ fun WeatherDaySelector(
Text(
modifier = Modifier.padding(start = 8.dp),
text = "${dateFormat.format(day.timestamp)} " +
"${convertTemperature(imperialUnits, day.minTemp)}° / " +
"${convertTemperature(imperialUnits, day.maxTemp)}°",
"${convertTemperature(useFahrenheit, day.minTemp)}° / " +
"${convertTemperature(useFahrenheit, day.maxTemp)}°",
softWrap = false,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
@@ -591,8 +601,8 @@ private fun formatTime(context: Context, timestamp: Long): String {
return DateUtils.formatDateTime(context, timestamp, DateUtils.FORMAT_SHOW_TIME)
}
private fun convertTemperature(imperialUnits: Boolean, temp: Double): Int {
return (if (imperialUnits) temp * 9.0 / 5.0 - 459.67 else temp + -273.15).roundToInt()
private fun convertTemperature(useFahrenheit: Boolean, temp: Double): Int {
return (if (useFahrenheit) temp * 9.0 / 5.0 - 459.67 else temp + -273.15).roundToInt()
}
@Composable
@@ -606,13 +616,13 @@ private fun formatWindSpeed(imperialUnits: Boolean, forecast: Forecast): String
}
@Composable
private fun formatPrecipitation(imperialUnits: Boolean, forecast: Forecast): String {
private fun formatPrecipitation(useInches: Boolean, forecast: Forecast): String {
if (forecast.precipProbability != null) {
return "${forecast.precipProbability} %"
}
val formatter = if (imperialUnits) DecimalFormat("#.##") else DecimalFormat("#.#")
val formatter = if (useInches) DecimalFormat("#.##") else DecimalFormat("#.#")
val precipUnit =
if (imperialUnits) stringResource(id = R.string.unit_inch_symbol) else stringResource(id = R.string.unit_millimeter_symbol)
if (useInches) stringResource(id = R.string.unit_inch_symbol) else stringResource(id = R.string.unit_millimeter_symbol)
return "${formatter.format(forecast.precipitation)} $precipUnit"
}

View File

@@ -2,11 +2,15 @@ package de.mm20.launcher2.ui.launcher.widgets.weather
import android.content.Context
import android.content.Intent
import android.icu.util.LocaleData
import android.icu.util.ULocale
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.*
import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.MeasurementSystem
import de.mm20.launcher2.preferences.weather.WeatherSettings
import de.mm20.launcher2.ui.settings.SettingsActivity
import de.mm20.launcher2.weather.DailyForecast
@@ -112,8 +116,23 @@ class WeatherWidgetVM : ViewModel(), KoinComponent {
val autoLocation = weatherSettings.autoLocation
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
val imperialUnits = weatherSettings.imperialUnits
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)
val measurementSystem = weatherSettings.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.System)
fun selectDay(index: Int) {
selectedDayIndex = min(index, forecasts.lastIndex)

View File

@@ -1,11 +1,6 @@
package de.mm20.launcher2.ui.settings.locale
import android.app.LocaleConfig
import android.content.Intent
import android.content.pm.PackageManager
import android.os.LocaleList
import androidx.activity.compose.LocalActivity
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -13,15 +8,14 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.capitalize
import androidx.core.app.GrammaticalInflectionManagerCompat
import androidx.core.net.toUri
import androidx.core.os.LocaleListCompat
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation3.runtime.NavKey
import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.preferences.MeasurementSystem
import de.mm20.launcher2.preferences.TimeFormat
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.preferences.ListPreference
@@ -29,10 +23,9 @@ 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 kotlinx.serialization.Serializable
import java.util.Locale
@Serializable
data object LocaleSettingsRoute: NavKey
data object LocaleSettingsRoute : NavKey
@Composable
fun LocaleSettingsScreen() {
@@ -40,11 +33,16 @@ fun LocaleSettingsScreen() {
val viewModel: LocaleSettingsScreenVM = viewModel()
val timeFormat by viewModel.timeFormat.collectAsStateWithLifecycle(null)
val measurementSystem by viewModel.measurementSystem.collectAsStateWithLifecycle(null)
val language = remember {
// The language that has been selected by the user, or null to use the system language
val selectedLocale = remember {
AppCompatDelegate.getApplicationLocales().get(0)
}
// The current language, including the resolved system language
val currentLocale = LocalResources.current.configuration?.locales[0]
PreferenceScreen(
title = stringResource(R.string.preference_screen_locale)
@@ -54,10 +52,11 @@ fun LocaleSettingsScreen() {
Preference(
icon = R.drawable.flag_24px,
title = stringResource(R.string.preference_language),
summary = if (language == null) {
summary = if (selectedLocale == null) {
stringResource(R.string.preference_value_system_default)
} else {
language.getDisplayName(language).replaceFirstChar { it.uppercase(language) }
selectedLocale.getDisplayName(selectedLocale)
.replaceFirstChar { it.uppercase(selectedLocale) }
},
enabled = isAtLeastApiLevel(33),
onClick = {
@@ -68,7 +67,7 @@ fun LocaleSettingsScreen() {
)
}
)
if (listOf("fr").contains(LocalResources.current.configuration?.locales[0]?.language)) {
if (listOf("fr").contains(currentLocale?.language)) {
ListPreference(
icon = R.drawable.wc_24px,
title = stringResource(R.string.preference_form_of_address),
@@ -95,6 +94,10 @@ fun LocaleSettingsScreen() {
)
)
}
}
}
item {
PreferenceCategory {
ListPreference(
icon = R.drawable.schedule_24px,
title = stringResource(R.string.preference_clock_widget_time_format),
@@ -108,6 +111,20 @@ fun LocaleSettingsScreen() {
stringResource(R.string.preference_clock_widget_time_format_24h) to TimeFormat.TwentyFourHour,
)
)
ListPreference(
icon = R.drawable.measuring_tape_24px,
title = stringResource(R.string.preference_measurement_system),
value = measurementSystem,
onValueChanged = {
if (it != null) viewModel.setMeasurementSystem(it)
},
items = listOf(
stringResource(R.string.preference_value_system_default) to MeasurementSystem.System,
stringResource(R.string.preference_measurement_system_metric) to MeasurementSystem.Metric,
stringResource(R.string.preference_measurement_system_uk) to MeasurementSystem.UnitedKingdom,
stringResource(R.string.preference_measurement_system_us) to MeasurementSystem.UnitedStates,
)
)
}
}
}

View File

@@ -1,6 +1,7 @@
package de.mm20.launcher2.ui.settings.locale
import androidx.lifecycle.ViewModel
import de.mm20.launcher2.preferences.MeasurementSystem
import de.mm20.launcher2.preferences.TimeFormat
import de.mm20.launcher2.preferences.ui.LocaleSettings
import org.koin.core.component.KoinComponent
@@ -13,4 +14,9 @@ class LocaleSettingsScreenVM: ViewModel(), KoinComponent {
fun setTimeFormat(timeFormat: TimeFormat) {
localeSettings.setTimeFormat(timeFormat)
}
val measurementSystem = localeSettings.measurementSystem
fun setMeasurementSystem(measurementSystem: MeasurementSystem) {
localeSettings.setMeasurementSystem(measurementSystem)
}
}

View File

@@ -93,15 +93,6 @@ fun WeatherIntegrationSettingsScreen() {
},
value = weatherProvider
)
val imperialUnits by viewModel.imperialUnits.collectAsState(false)
SwitchPreference(
title = stringResource(R.string.preference_imperial_units),
summary = stringResource(R.string.preference_imperial_units_summary),
value = imperialUnits,
onValueChanged = {
viewModel.setImperialUnits(it)
}
)
}
}

View File

@@ -35,11 +35,6 @@ class WeatherIntegrationSettingsScreenVM : ViewModel(), KoinComponent {
it?.let { pluginService.getPluginWithState(it) } ?: flowOf(null)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
val imperialUnits = weatherSettings.imperialUnits
fun setImperialUnits(imperialUnits: Boolean) {
weatherSettings.setImperialUnits(imperialUnits)
}
val autoLocation = weatherSettings.autoLocation
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M540,800L280,800Q247,800 223.5,776.5Q200,753 200,720L200,460Q200,318 299,219Q398,120 540,120Q682,120 781,219Q880,318 880,460Q880,602 781,701Q682,800 540,800ZM540,720Q648,720 724,644Q800,568 800,460Q800,352 724,276Q648,200 540,200Q432,200 356,276Q280,352 280,460L280,720Q280,720 280,720Q280,720 280,720L540,720ZM540,600Q598,600 639,559Q680,518 680,460Q680,402 639,361Q598,320 540,320Q482,320 441,361Q400,402 400,460Q400,518 441,559Q482,600 540,600ZM540,520Q515,520 497.5,502.5Q480,485 480,460Q480,435 497.5,417.5Q515,400 540,400Q565,400 582.5,417.5Q600,435 600,460Q600,485 582.5,502.5Q565,520 540,520ZM120,800Q103,800 91.5,788.5Q80,777 80,760L80,640Q80,623 91.5,611.5Q103,600 120,600Q137,600 148.5,611.5Q160,623 160,640L160,760Q160,777 148.5,788.5Q137,800 120,800ZM540,460L540,460Q540,460 540,460Q540,460 540,460L540,460Q540,460 540,460Q540,460 540,460Q540,460 540,460Q540,460 540,460Q540,460 540,460Q540,460 540,460Z"/>
</vector>

View File

@@ -870,7 +870,7 @@
<string name="preference_screen_locale_summary">Sprache, Einheiten, Zeitformat</string>
<string name="preference_language">Sprache</string>
<string name="preference_value_system_default">Systemstandard</string>
<string name="preference_form_of_address">Anrederform</string>
<string name="preference_form_of_address">Anredeform</string>
<string name="preference_form_of_address_neutral">Neutral</string>
<string name="preference_form_of_address_fem">Feminin</string>
<string name="preference_form_of_address_masc">Maskulin</string>

View File

@@ -1063,4 +1063,8 @@
<string name="preference_form_of_address_neutral">Neutral</string>
<string name="preference_form_of_address_fem">Feminine</string>
<string name="preference_form_of_address_masc">Masculine</string>
<string name="preference_measurement_system">Measurement system</string>
<string name="preference_measurement_system_metric">Metric</string>
<string name="preference_measurement_system_uk">United Kingdom</string>
<string name="preference_measurement_system_us">United States</string>
</resources>

View File

@@ -195,6 +195,7 @@ data class LauncherSettingsData internal constructor(
@JsonNames("clockWidgetTimeFormat")
val localeTimeFormat: TimeFormat = TimeFormat.System,
val localeMeasurementSystem: MeasurementSystem = MeasurementSystem.System,
) {
@@ -433,4 +434,13 @@ enum class TimeFormat {
@SerialName("system") System,
@SerialName("12h") TwelveHour,
@SerialName("24h") TwentyFourHour
}
@Serializable
enum class MeasurementSystem {
@SerialName("system") System,
@SerialName("metric") Metric,
@SerialName("uk") UnitedKingdom,
@SerialName("us") UnitedStates,
}

View File

@@ -1,6 +1,7 @@
package de.mm20.launcher2.preferences.ui
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.MeasurementSystem
import de.mm20.launcher2.preferences.TimeFormat
import kotlinx.coroutines.flow.map
@@ -16,4 +17,13 @@ class LocaleSettings internal constructor(
it.copy(localeTimeFormat = timeFormat)
}
}
val measurementSystem
get() = launcherDataStore.data.map { it.localeMeasurementSystem }
fun setMeasurementSystem(measurementSystem: MeasurementSystem) {
launcherDataStore.update {
it.copy(localeMeasurementSystem = measurementSystem)
}
}
}

View File

@@ -1,7 +1,11 @@
package de.mm20.launcher2.preferences.weather
import android.icu.util.LocaleData
import android.icu.util.ULocale
import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.preferences.LatLon
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.MeasurementSystem
import de.mm20.launcher2.preferences.ProviderSettings
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
@@ -181,14 +185,7 @@ class WeatherSettings internal constructor(
}
}
val imperialUnits = launcherDataStore.data.map { it.weatherImperialUnits }
.distinctUntilChanged()
fun setImperialUnits(imperialUnits: Boolean) {
launcherDataStore.update {
it.copy(
weatherImperialUnits = imperialUnits,
)
}
}
val measurementSystem = launcherDataStore.data.map {
it.localeMeasurementSystem
}.distinctUntilChanged()
}