diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/weather/WeatherWidget.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/weather/WeatherWidget.kt index 64827d623..b4195c17e 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/weather/WeatherWidget.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/weather/WeatherWidget.kt @@ -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, 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, 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" } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/weather/WeatherWidgetVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/weather/WeatherWidgetVM.kt index 5a3e56b7f..edc33ff1e 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/weather/WeatherWidgetVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/weather/WeatherWidgetVM.kt @@ -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) diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/locale/LocaleSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/locale/LocaleSettingsScreen.kt index 2b363cba4..ed280120c 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/locale/LocaleSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/locale/LocaleSettingsScreen.kt @@ -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, + ) + ) } } } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/locale/LocaleSettingsScreenVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/locale/LocaleSettingsScreenVM.kt index 7d970a6c3..cfe4469fd 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/locale/LocaleSettingsScreenVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/locale/LocaleSettingsScreenVM.kt @@ -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) + } } \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/weather/WeatherIntegrationSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/weather/WeatherIntegrationSettingsScreen.kt index 90f6bca8c..d78a0d124 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/weather/WeatherIntegrationSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/weather/WeatherIntegrationSettingsScreen.kt @@ -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) - } - ) } } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/weather/WeatherIntegrationSettingsScreenVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/weather/WeatherIntegrationSettingsScreenVM.kt index c81733805..6008c5aec 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/weather/WeatherIntegrationSettingsScreenVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/weather/WeatherIntegrationSettingsScreenVM.kt @@ -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) diff --git a/core/base/src/main/res/drawable/measuring_tape_24px.xml b/core/base/src/main/res/drawable/measuring_tape_24px.xml new file mode 100644 index 000000000..289ed0396 --- /dev/null +++ b/core/base/src/main/res/drawable/measuring_tape_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/core/i18n/src/main/res/values-de/strings.xml b/core/i18n/src/main/res/values-de/strings.xml index c7cde97ad..7e4a39808 100644 --- a/core/i18n/src/main/res/values-de/strings.xml +++ b/core/i18n/src/main/res/values-de/strings.xml @@ -870,7 +870,7 @@ Sprache, Einheiten, Zeitformat Sprache Systemstandard - Anrederform + Anredeform Neutral Feminin Maskulin diff --git a/core/i18n/src/main/res/values/strings.xml b/core/i18n/src/main/res/values/strings.xml index c6f3e0762..e1cbbed8a 100644 --- a/core/i18n/src/main/res/values/strings.xml +++ b/core/i18n/src/main/res/values/strings.xml @@ -1063,4 +1063,8 @@ Neutral Feminine Masculine + Measurement system + Metric + United Kingdom + United States diff --git a/core/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherSettingsData.kt b/core/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherSettingsData.kt index f9a339925..7125ba818 100644 --- a/core/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherSettingsData.kt +++ b/core/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherSettingsData.kt @@ -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, } \ No newline at end of file diff --git a/core/preferences/src/main/java/de/mm20/launcher2/preferences/ui/LocaleSettings.kt b/core/preferences/src/main/java/de/mm20/launcher2/preferences/ui/LocaleSettings.kt index add9c5e7a..d9772ae3f 100644 --- a/core/preferences/src/main/java/de/mm20/launcher2/preferences/ui/LocaleSettings.kt +++ b/core/preferences/src/main/java/de/mm20/launcher2/preferences/ui/LocaleSettings.kt @@ -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) + } + } } \ No newline at end of file diff --git a/core/preferences/src/main/java/de/mm20/launcher2/preferences/weather/WeatherSettings.kt b/core/preferences/src/main/java/de/mm20/launcher2/preferences/weather/WeatherSettings.kt index f7bbc60a3..f9f489793 100644 --- a/core/preferences/src/main/java/de/mm20/launcher2/preferences/weather/WeatherSettings.kt +++ b/core/preferences/src/main/java/de/mm20/launcher2/preferences/weather/WeatherSettings.kt @@ -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() } \ No newline at end of file